In a Java-based CLI application, I'm using the following libraries
- Guice for dependency injection
- Lightbend Typesafe Config to handle the configurable options
- Typesafe Config Guice to tie these two together and allow for the configuration values to be injected where needed
- picocli to parse the command line options
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:
- In the Logging Mixin used to configure the log4j2 component - this is taken from the picocli logging_mixin_advanced example.
- In the subcommands - currently in an abstract superclass of all subcommands, but that might change in the future.
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?
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 thejava
tool.)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.