picocli icon indicating copy to clipboard operation
picocli copied to clipboard

Support binding options and parameters without reflection

Open jameskleeh opened this issue 4 years ago • 5 comments

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.

jameskleeh avatar Apr 22 '20 16:04 jameskleeh

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, the java.time classes and some java.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.)

remkop avatar Apr 22 '20 21:04 remkop

@jameskleeh Did this answer your question?

remkop avatar Apr 23 '20 13:04 remkop

@remkop Yes - I appreciate the detailed response! Going to consider the programmatic approach

jameskleeh avatar Apr 23 '20 13:04 jameskleeh

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.

remkop avatar Apr 23 '20 14:04 remkop

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?

remkop avatar Apr 23 '20 14:04 remkop