Before using a cli I would have a starter class which calls my ApplicationPropertiesProvider class (which reads my properties file) and then kicks off the business logic. So there was a separation, the ApplicationPropertiesProvider just had one job.
Now with picocli, the guide/documentation states I have to use CommandLine.run(objectToPopulate, args) or CommandLine.call(objectToPopulate, args). Therefore the class being populated with the cli parameters (ApplicationPropertiesProvider) has to implement Runnable or Callable. Now I could just paste my kick-off code of the Starter class into the run() or call() method and abandon the Starter class then. But I don't like that, I want to separate between a class just holding the properties and my Starter class.
A kind of dirty workaround I thought ofand shown in my example below would be to pass the arguments from the main method to my Starter class' constructor, populate the ApplicationPropertiesProvider with CommandLine.run() but only implement an empty run() or call() method there so it will immediately return to my Starter class where I kick off the business logic then. That would be the result I ask for (separation), but that way it seems really stupid.
Also another question which just came up: If I have the standard case of having multiple classes containing business code and also their own properties (instead of a single property providing class): Is it possible to populate multiple different classes with one cli call, i.e. calling "test.jar command --a --b" where parameter "a" goes straight to an instance of class "X" and "b" goes to an instance of "Y"?
public class Starter {
public static void main(String[] args) {
new Starter(args);
}
public Starter(String[] args) {
app = ApplicationPropertiesProvider.getInstance();
CommandLine.run(app, args);
//then kick off the business logic of the application
}
}
@Command(...)
public class ApplicationPropertiesProvider implements Runnable {
//annotated properties
@Option(...)
private String x;
@Override
public void run() { }
The run
and call
methods are convenience methods to allow applications to reduce their boilerplate code. You don't need to use them. Instead, you can use the parse
or parseArgs
method. This looks something like this:
1 @Command(mixinStandardHelpOptions = true)
2 public class ApplicationPropertiesProvider { // not Runnable
3 //annotated properties
4 @Option(...)
5 private String x;
6 // ...
7 }
8
9 public class Starter {
10 public static void main(String[] args) {
11 ApplicationPropertiesProvider app = ApplicationPropertiesProvider.getInstance();
12 try {
13 ParseResult result = new CommandLine(app).parseArgs(args);
14 if (result.isUsageHelpRequested()) {
15 cmd.usage(System.out);
16 } else if (result.isVersionHelpRequested()) {
17 cmd.printVersionHelp(System.out);
18 } else {
19 new Starter(app); // run the business logic
20 }
21 } catch (ParameterException ex) {
22 System.err.println(ex.getMessage());
23 ex.getCommandLine().usage(out, ansi);
24 }
25 }
26
27 public Starter(ApplicationPropertiesProvider app) {
28 // kick off the business logic of the application
29 }
30 }
This is fine, it is just that lines 11-24 are boilerplate code. You can omit this and let picocli do this work for you by letting the annotated object implement Runnable or Callable.
I understand your point about separation of concerns and have different classes for the business logic and the class that has the properties. I have a suggestion, but first let me answer your seconds question:
Is it possible to populate multiple different classes with one cli call?
Picocli supports "Mixins" that allow you to do this. For example:
class A {
@Option(names = "-a") int aValue;
}
class B {
@Option(names = "-b") int bValue;
}
class C {
@Mixin A a;
@Mixin B b;
@Option(names = "-c") int cValue;
}
// populate C's properties as well as the nested mixins
C c = CommandLine.populate(new C(), "-a=11", "-b=22", "-c=33");
assert c.a.aValue == 11;
assert c.b.bValue == 22;
assert c.cValue == 33;
Now, let's put all this together:
class A {
@Option(names = "-a") int aValue;
@Option(names = "-b") int bValue;
@Option(names = "-c") int cValue;
}
class B {
@Option(names = "-x") int xValue;
@Option(names = "-y") int yValue;
@Option(names = "-z") int zValue;
}
class ApplicationPropertiesProvider {
@Mixin A a;
@Mixin B b;
}
class Starter implements Callable<Void> {
@Mixin ApplicationPropertiesProvider properties = ApplicationPropertiesProvider.getInstance();
public Void call() throws Exception {
// business logic here
}
public static void main(String... args) {
CommandLine.call(new Starter(), args);
}
}
This gives you separation of concerns: properties are located in the ApplicationPropertiesProvider
, business logic is in the Starter
class.
It also allows you to group properties that logically belong together into separate classes, instead of having a single dumping ground in ApplicationPropertiesProvider
.
The Starter
class implements Callable
; this allows you to omit the boilerplate logic above and start your application in a single line of code in main
.