picocli
picocli copied to clipboard
Support binding options and parameters without reflection
Currently something like the following requires special notation to work with GraalVM:
@CommandLine.Command(name = "create-controller", description = "Creates a controller and associated test")
public class CreateControllerCommand extends CodeGenCommand {
@CommandLine.Parameters(paramLabel = "CONTROLLER-NAME", description = "The name of the controller to create")
String controllerName;
The controllerName field is bound via reflection. There should be a hook to allow for the user to control how the property is bound to avoid reflection.
Hi James, thanks for your question!
Yes, this facility already exists in picocli's programmatic API, see the Programmatic API and Bindings part of my answer below, but I do wonder why you need it.
Annotations and reflection in picocli
The example you gave uses annotations. When an application uses picocli's annotations framework, picocli uses reflection in multiple places:
- first to read these annotations when constructing a
CommandSpec
model, - then for some built-in type converters for types that are not available in Java 5 (like
java.nio.file.Path
, thejava.time
classes and somejava.sql
classes), - and finally to apply values that were matched on the command line to
@Option
- and@Parameters
-annotated fields and methods.
You mention this last usage of reflection in picocli but be aware this is not the only usage for applications that use the annotations API.
Current Solution
The solution that picocli offers out of the box for creating native executables with GraalVM (as I am sure you are aware) is to generate a reflect-config.json
file. (The picocli-codegen
module contains a ReflectionConfigGenerator
class that can be invoked directly or invoked by picocli's annotation processor for this.)
Future Solutions
I am considering a different approach for processing annotations, without reflection, in a future release of picocli. The basic idea would be to generate code at compile time; I believe this is similar to the approach Micronaut takes. Some early thoughts on this topic are in this ticket. This is at the conceptual stage and would require quite a bit of work to design, implement, test and document.
Programmatic API
Applications can avoid reflection altogether by using picocli's programmatic API. This is a different programming model that does not use the annotations.
Bindings
And now we finally get to answer your question! :-)
The programmatic API manual has a section on bindings, but in a nutshell:
An option or positional parameter is represented in picocli with the ArgSpec
class (superclass of OptionSpec
and PositionalParamSpec
). Every ArgSpec
instance has customizable getter and setter bindings. These bindings are invoked when the picocli parser matches a value for that option or positional parameter on the command line.
Applications can provide custom IGetter and ISetter implementations, to get complete control over what happens when a value is matched.
You would use it as follows:
CommandSpec spec = CommandSpec.create();
spec.addPositional(PositionalParamSpec.builder()
.paramLabel("CONTROLLER-NAME")
.description("The name of the controller to create")
.getter(() -> { return getCreateControllerCommandInstance().controllerName; })
.setter((value) -> { getCreateControllerCommandInstance().controllerName = value; })
.type(String.class).auxiliaryTypes(String.class) // for type conversion
.build());
// assumes there is a getCreateControllerCommandInstance() method
// that returns your CreateControllerCommand instance
This example uses CommandSpec.create()
to create an "empty" CommandSpec
, and programmatically/manually add options and positional parameters to it. This CommandSpec
object can then be used to construct a CommandLine
instance, on which the application can then call parseArgs
or execute
.
If the CommandLine
constructor is called with a CommandSpec
instance, it will not use reflection to create a model, but just use the specified CommandSpec
.
By contrast, if the CommandLine
constructor is called with any other object, it will use reflection to contruct a CommandSpec
instance. (If you want to use reflection to contruct a CommandSpec
instance from an annotated class via the programmatic API, then pass that annotated class or an instance of it to CommandSpec::forAnnotatedObject
.)
@jameskleeh Did this answer your question?
@remkop Yes - I appreciate the detailed response! Going to consider the programmatic approach
Ok, let me know if you run into any snags or have questions. The programmatic API docs are not as detailed as the main user manual; let me know if it is missing stuff.
Wild idea: depending on how many commands and options your application has, it may be just as easy to generate the source code. Building such a code generator is on the picocli roadmap: see https://github.com/remkop/picocli/issues/539 (also related: #750).
Picocli can already build a CommandSpec
from the annotations at compile time.
What is missing is to generate the source code (using the programmatic API) from this CommandSpec
that would build the same CommandSpec
again at runtime without reflection.
So, basically, given an annotated class:
@CommandLine.Command(name = "create-controller", description = "Creates a controller and associated test")
public class CreateControllerCommand extends CodeGenCommand {
@CommandLine.Parameters(paramLabel = "CONTROLLER-NAME", description = "The name of the controller to create")
String controllerName;
Generate something like this at compile time:
CommandSpec spec = CommandSpec.create();
spec.name("create-controller").description("Creates a controller and associated test");
spec.addPositional(PositionalParamSpec.builder()
.paramLabel("CONTROLLER-NAME")
.description("The name of the controller to create")
.getter(() -> { return getCreateControllerCommandInstance().controllerName; })
.setter((value) -> { getCreateControllerCommandInstance().controllerName = value; })
.type(String.class).auxiliaryTypes(String.class) // for type conversion
.build());
The Micronaut team may have more experience in this than me. Would you be interested in collaborating on https://github.com/remkop/picocli/issues/539?