Search code examples
dependency-injectiontypesafe-configpicocli

Circular initialization dependency with picocli, typesafe config and Guice


In a Java-based CLI application, I'm using the following libraries

The application has different actions that can be executed. I have implemented these using subcommands. The entire setup is as follows:

The main class is annotated with the subcommand classes. In the main method, I first load and validate the configuration, then create an IFactory instance that in turn creates the Guice injector and configures it with modues for my application as well as the config injection module mentioned above. The factory is used to instantiate the subcommand classes as shown in the picocli docs, because the subcommands already need some stuff injected, including instances that in turn need access to configuration parameters. Back in the main class, I hand over the processing to picocli.

There are two places where command line options are currently defined:

All of the above is working as desired so far, and I'd really like to thank everyone involved in creating this kind of infrastructure.

I'm now faced with the following issue: Typesafe Config allows for setting individual configuration options from the command line by specifying system properties with -Dmy.config.par=foobar, which is what I would like to use. This works perfectly as long as I either start my application from with the IDE or execute it from the command line with java -classpath $CP -Dmy.config.par=foobar org.x2vc.Checker <otherParams> - i.e., set the property before the main class is specified. For a distributable version, this is not an option because the classpath can become very long. I'm currently using the Maven Application Assembler plug-in to generate startup scripts for different platforms, which works really well - except for the part that I can't easily add -D parameters before the class name.

I am trying to implement the approach in the picocli documentation - essentially extend the command line options by a -D option and set the system properties within my application. I can't seem to get it working however. As far as I've understood, I have a kind of logical circular dependecy here:

  • picocli requires instances of the subcommands for command line parameter parsing and validation.
  • The subcommands are created using DI, therefore the injector has to be configured before that.
  • The injector needs to be configured with the typesafe config module to provide access to the configuration parameters.
  • The configuration must be loaded before that to initialize the module.
  • Processing of the -D option by picocli only takes place after all of the above has happened.
  • At this point, the (immutable!) configuration instance is already distributed far and wide thoughout the application.

I'm unsure what approach to take from here. I would like to avoid modifying the shell scripts if possible - testing these in various environments would be a huge additional task. The only option I currently see is to move the Guice initialization into the subcommands - is there a better option that I have overlooked?


Solution

  • It does sound like you need to specify these system properties before invoking picocli, maybe even before creating the IFactory.

    Some ideas:

    Idea 1:

    Users who run your application on Java 11 can use java argument files when they execute the java command. (This is similar to picocli's @file feature, but this is built in to the java tool.)

    java @myargs org.x2vc.Checker <otherParams>
    
    # contents of myargs:
    -classpath $CP
    -Dmy.config.par=foobar
    -Dmy.config.par2=foobar2
    -Dmy.config.par3=foobar3
    

    This solves the problem of the command line becoming too long.

    Idea 2:

    Use picocli twice: once to parse out the command line arguments, and once again for the actual application. The documentation has an example of two-phase parsing for another use case.

    In the first phase you are only interested in system properties, so you don't need a factory, or any logging or any of that. You just create a simple @Command class that detects -D properties and ignores everything else.

    After these are successfully parsed from the command line and System.setProperty has been invoked for each of them, phase 1 is complete.

    Phase 2 is basically your application as you described above: create the factory, use Guice, instantiate a picocli CommandLine with that factory, etc.