Quarkus Command mode with Picocli
After this article, you’ll be able to write beautiful command-line tools for Quarkus with Picocli. This is the continuation of the first article about Quarkus mode, if you haven’t seen it, please read it first.
Now that you know how to use command mode, it’s time to prettify it! We gonna use Picocli, which makes cli tools fun to build.
For better copy-paste experience go to Quarkify website
Initial setup
If you are lazy or just want to see the final result clone this repo on the master branch and just look through the next steps.
First and foremost, let’s clone our previous project, you don’t need to do it if you already followed our previous article.
git clone --branch clean https://github.com/quarkifynet/quarkus-command-mode-picocli.git
Let’s recap shortly what’s inside. Besides our GreetingResource
and GreetingService
, we have GreetingApplication
that's executed either from maven or command line, let's check that it's working.
./mvnw compile quarkus:dev -Dquarkus.args="--greet RedHat"
If you saw logger’s info message “Hello RedHat” then you’re on the right track. Let’s add a few more dependencies that will help us convert our command support into OOP magic.
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-shared-utils</artifactId>
<version>3.2.1</version>
</dependency>
First dependency is picocli itself, second one will be used to parse single string into multiple to feed them into picocli.
Root command
Let’s create our first command, It’ll be root command that will help us route to different commands, such as help and greet.
Nothing special, however, we need to clarify some moments here.
- Each Picocli command should be annotated with @Command, there are many parameters that you can use, for root command we use
subcommands
- Any(except root) Picocli command should implement Runnable or Callable interface, Callable<Integer> can be useful if you want to return error code back to the terminal.
- We use built-in
CommandLine.HelpCommand
so that we don't re-invent the wheel.
Greeting command with DI
That’s all good, but how do we inject our beans into the command? That’s easy enough, let’s create our GreetingCommand
This one is more interesting, let’s see what’s going on.
- We use
@Dependent
annotation so that we create Bean from this class, that we can later inject into e.gGreetingApplication
. - In
@Command
, we specifyname
of the command, this will be used to identify which command to call, e.g help or this one. - We also specify
mixinStandardHelpOptions
to make help info for this command, as well as a description that will be used there - We created
String name
field, which will be parsed from--name={}
or-n {}
, argument passed to our command. - We Injected
GreetingService
to handle all the greeting work since we have@Dependent
annotation. - And, of course, we have
call()
method that handles all the logic written to this command.
Picocli will look at all arguments, as well as the structure of the command tree, and decide which command it should execute since we have 1 root and 2 subcommands, it will look at first argument passed, and compare it with the name of the command. Name of command we specified in @Command
annotation, but you can also specify it directly in code.
Use new commands in GreetingApplication
We made a really beautiful command, but haven’t used it anywhere yet, let’s fix it. We already have GreetingApplication
, we just need to remove all the code that we wrote in run
function and re-write it a little bit.
Let’s follow this class line by line.
@Inject
GreetingCommand greetingCommand;
We inject our GreetingCommand, that we annotated with @Dependable
. When we pass it later to CommandLine
object, it already has all the dependencies(in our case GreetingService) injected and ready to use.
@Override
public int run(String... args) throws Exception {
if (args.length == 0) {
Quarkus.waitForExit();
return 0;
}
This part haven’t changed, if there’s 0 arguments — we just assume that user wants to start complete Quarkus applciation.
if (args.length == 1) {
args = CommandLineUtils.translateCommandline(args[0]);
}
This part is new, you might’ve noticed that if you use maven command, you receive commands as a single string, this tool will split it into separate strings. This step is required for picocli to work correctly, since it awaits each argument on a separate string. I used Maven shared utils to parse it, they’re safe to use with native builds.
return new CommandLine(new QuarkusCommand())
.addSubcommand(greetingCommand)
.execute(args);
And the final part, we create new CommandLine
object, in which we pass QuarkusCommand
, we create a new instance of it since we don't need any Dependency injection, so that's okay. then we call addSubcommand
and pass our injected greetingCommand
into it. And the final step, of course, is to .execute()
it with arguments that we passed or parsed. The rest is by picocli magic.
Let’s call our code and see how it’ll work.
./mvnw compile quarkus:dev -Dquarkus.args="help"
Output:
...
Usage: <main class> [COMMAND]
Commands:
help Displays help information about the specified command
greet Greet person by their name
...
All right, let’s call greet with help option
./mvnw compile quarkus:dev -Dquarkus.args="greet --help"
Output:
Usage: <main class> greet [-hV] [-n=]Greet person by their name
-h, --help Show this help message and exit.
-n, --name=<name> Specify which user to greet
-V, --version Print version information and exit.
Nice, now any developer can see how to use your commands. Easy to understand and helpful. Let’s finally greet our user
./mvnw compile quarkus:dev -Dquarkus.args="greet --name=Quarkus"
In output, of course, you’ll see hello Quarkus
.
Interaction with database
If you are lazy or just want to see the final result of this section execute
git checkout feature/db_update
and just look through the next steps.
Well, that was a kinda useful example. But real-world applications won’t do this kind of operation (or rarely). Real-world apps will do database-related tasks, such as run some statistics or update some values in db. Well, let’s put our greeting phrase into a database and use the command to update this. Let’s add some dependencies and properties to our project.
Firstly let’s add hibernate+panache and h2 jdbc driver into pom.xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
And let’s create file src/main/resource/application.properties
with next lines:
quarkus.datasource.db-kind=h2
quarkus.datasource.username=username-default
quarkus.datasource.jdbc.url=jdbc:h2:./test
quarkus.hibernate-orm.database.generation=update
Nice, now we have a h2 database ready for use. We only need to create our entity, I decided to create Setting
entity with key and value(and Id of course), here's full class:
Nothing really special here, we interested in PanacheEntityBase
to make this example easy for the article.
In the next step, we need to adapt our GreetingService
to use our Setting class.
@ApplicationScoped
public class GreetingService {
public String greeting(String name) {
return Setting.find("key = 'greet_phrase'").<Setting>firstResult().value + name;
}
}
We gonna use greet_phrase
key to get our greeting name. As I've said, we use Panache's methods to get data from the database. Now we're finally ready to create our new command. Here's full code for SetCommand
:
This one is similar to our previous GreetingCommand
, but it has some differences:
- We use
@Paramteres
annotation for input, they differ from@Options
ability to use them without any hyphens. We also created enumTableType
at the bottom of this class and use it as a first parameter to our command, picocli supports a super large amount of input type, and enum is one of them. - We use @Transactional because we want to write to the database in our
run()
method. Without this method will fail. Because we use@Dependent
annotation, transactional annotations will work. - We switch between different TableTypes(in our case only one, so we use if), and if it’s
SETTING
we find or create a new Setting entity, update key and value, and persist it.
Last part that is left is to add few lines to our GreetingApplication
. Let's firstly add inject of our SetCommand
@Inject
SetCommand setCommand;
And let’s just add it as a subcommand to our CommandLine
return new CommandLine(new QuarkusCommand())
.addSubcommand(greetingCommand)
.addSubcommand(setCommand)
.execute(args);
That’s it. Now we can modify our greeting phrase from the command line. I would say that doing it via the database will probably be easier. But if you’ll have some more logic or complex operations(e.g analyze some data by connecting to some external service, and then storing this data back to the database) similar commands will come in handy. Let’s try it out.
./mvnw compile quarkus:dev -Dquarkus.args="set SETTING greet_phrase 'Hello '"
This will set initial phrase, as it was before.
Now you can press Ctrl + C
and execute ./mvnw quarkus:dev
to start our dev server. With this let's curl into greeting endpoint(or you can execute our greet
command as a faster option)
curl http://localhost:8080/hello/greeting/Quarkus
You’ll see the usual phrase Hello Quarkus
. Now, let's stop our quarkus dev server and execute command with different value:
./mvnw compile quarkus:dev -Dquarkus.args="set SETTING greet_phrase 'Guten Tag '"
This time we changed greeting phrase to german Guten tag. Let’s re-run ./mvnw quarkus:dev and curl the same endpoint again.
curl http://localhost:8080/hello/greeting/Quarkus
This will output, as expected Guten Tag Quarkus
. In a real app, you don't need to stop and restart the app to execute a command, but by default, H2 has a lock on the file, and so we can use only one access at a time.
In conclusion
Picocli is a good part of your app if you want to introduce a great command structure. Even though I have a cold heart for Laravel Php framework, I do want to say that their command support done really well.
When app is small, you probably won’t need such a feature, or you’ll go with super simple setup, however when you have a complex app that needs to do some logic that shouldn’t be exposed to API(and you don’t want to bloat every command with the main method) picocli is the right way to go. It might be also a good idea to disable resteasy so that you can run commands while the main server is running, this way you can schedule some commands if needed.
Have you ever used commands in your project? How can you compare it to Quarkus?
Originally published at https://quarkify.net on April 27, 2020.