A Quarkus Command Line Application
Sunday, October 10, 2021 |Most people know Quarkus as a great way to build fast, scalable microservices. What many may not be aware of, however, is that Quarkus can also be used to build command line applications as well. In this post, we’ll take a look at how we can leverage the Quarkus ecosystem we already know to build a command line utility quickly and easily.
The command line application we’ll build actually already exists. The GitHub user techpavan has a utility, mvn-repo-cleaner, that can be used to clean up old, unused artifacts from the local Maven repository. It’s a great utility, but it hasn’t been updated in at least a couple of years. Using this as a practical target, then, let’s see what Quarkus can offer.
Quarkus' CLI support is based on Picocli. Using Quarkus' project generation site, we can bootstrap our app:
-
Specify the groupId:
com.steeplesoft
-
Specify the artifact name:
mvn-repo-cleaner
-
Select the build tool:
Maven
-
Search for
pico
and select the library -
Select
No
for Starter Code -
Click
Generate your application
Since we’re migrating/porting an existing CLI utility, we already have a nice, practical command to implement, which means
all we need to do is copy the existing classes in techpavan’s upstream project: ArgData
, CleanM2
, and FileInfo
. These
classes require some dependencies we haven’t declared yet, so let’s add those now:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.8.2</version>
</dependency>
The first class we need to update is CleanM2
. This class will be the entry point into our utility. Since the upstream
project is based on JCommander, our first step is to remove the related imports. We also need to annotate the class so
Picocli knows we’re defining a new command in this class:
1
2
3
@CommandLine.Command
public class CleanM2 implements Runnable {
// ...
We’ve also added an implements Runnable
, as Picocli requires that each command be Runnable
. The next step is to
modify the class' public void main
, making it implement the run()
method from the interface:
1
2
3
- public static void main(String[] args) {
+ @Override
+ public void run() {
We now have our Picocli command, but it could use some parameters, which are defined currently in ArgData
. Like CleanM2
,
this class heavily uses JCommander APIs. Fortunately, the APIs are pretty similar, so it’s mostly a matter of changing the
imports
1
2
3
4
5
- @Parameter(names = {"--path", "-p"}, description = "Path to m2 directory, if using a custom path.")
- private String m2Path;
+ @CommandLine.Option(names = {"--path", "-p"},
+ description = "Path to m2 directory, if using a custom path.")
+ protected String m2Path;
and then updating the annotations on the parameters. You can actually update them all at once with a simple search and replace: @Parameter
→ @CommandLine.Option
.
To make these options available to the command, we have two options: copy/move each of these properties to CleanM2
or
have CleanM2
extend ArgData
. I chose to go with the second option to reduce the number of changes from the upstream
project:
1
2
@CommandLine.Command
public class CleanM2 extends ArgData implements Runnable {
CleanM2
will still have quite a few compilation errors related to the parameter properties. I won’t go over those here,
as they’re pretty simple to update (you can also "cheat" by looking at the GitHub repo for this project.) I’ve also taken the opportunity to remove all of the
static
modifiers. That’s more of a personal preference than a technical requirement, so feel free not to do the same
if that’s your preference.
With those changes done, we’re ready to build and run this:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ mvn clean package
...
$ ls -alh target/
total 20K
drwxrwxr-x 1 jdlee jdlee 226 Oct 10 20:44 .
drwx--x--x. 1 jdlee jdlee 244 Oct 10 20:44 ..
drwxrwxr-x 1 jdlee jdlee 72 Oct 10 20:44 classes
drwxrwxr-x 1 jdlee jdlee 22 Oct 10 20:44 generated-sources
drwxrwxr-x 1 jdlee jdlee 28 Oct 10 20:44 maven-archiver
drwxrwxr-x 1 jdlee jdlee 42 Oct 10 20:44 maven-status
-rw-rw-r-- 1 jdlee jdlee 16K Oct 10 20:44 mvn-repo-cleaner-1.0.0-SNAPSHOT.jar
drwxrwxr-x 1 jdlee jdlee 112 Oct 10 20:44 quarkus-app
-rw-rw-r-- 1 jdlee jdlee 117 Oct 10 20:44 quarkus-artifact.properties
As things stand out of the box, to run our command, you would do something like this:
1
2
$ java -jar target/quarkus-app/quarkus-run.jar --help
...
While that works, shipping that is not ideal. We can fix that by changing how Quarkus packages the app. The default for
Quarkus is the "fast jar" format, which we see above in target/quarkus-app
. What we’d like is a fat/uber jar, where
our utility and all of its dependencies are in one archive. To do that with Quarkus, we need to configure the build
using src/main/resources/application.properties
:
1
quarkus.package.type=uber-jar
Now, if we rebuild:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ mvn clean package
...
$ ls -alh target/
total 4.0M
drwxrwxr-x 1 jdlee jdlee 286 Oct 10 20:53 .
drwx--x--x. 1 jdlee jdlee 244 Oct 10 20:53 ..
drwxrwxr-x 1 jdlee jdlee 72 Oct 10 20:53 classes
drwxrwxr-x 1 jdlee jdlee 22 Oct 10 20:53 generated-sources
drwxrwxr-x 1 jdlee jdlee 28 Oct 10 20:53 maven-archiver
drwxrwxr-x 1 jdlee jdlee 42 Oct 10 20:53 maven-status
-rw-rw-r-- 1 jdlee jdlee 16K Oct 10 20:53 mvn-repo-cleaner-1.0.0-SNAPSHOT.jar.original
-rw-rw-r-- 1 jdlee jdlee 4.0M Oct 10 20:53 mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar
-rw-rw-r-- 1 jdlee jdlee 122 Oct 10 20:53 quarkus-artifact.properties
We now have a 4M archive, mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar
, which we can run with:
1
2
$ java -jar target/mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar --help
...
That gives us a single file to ship around to users' machines, which is definitely an improvement. It still requires a JVM, though. While that’s not necessarily an issue for this command, which is clearly targeted at developers, your command line utility may not be. This is where Quarkus starts to shine: we’re going to build a native image, which Quarkus makes super easy, barely an inconvenience. To do so, though, we will need GraalVM installed, which I’ve done using sdkman!.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ sdk install java 21.2.0.r16-grl
$ sdk use java 21.2.0.r16-grl
$ gu install native-image
$ mvn -Pnative package
...
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] /home/jdlee/.sdkman/candidates/java/21.2.0.r16-grl/bin/native-image -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Duser.language=en -J-Duser.country=US -J-Dfile.encoding=UTF-8 -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI -H:+AllowFoldMethods -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http -H:NativeLinkerOption=-no-pie -H:-UseServiceLoaderFeature -H:+StackTrace -H:-ParseOnce mvn-repo-cleaner-1.0.0-SNAPSHOT-runner -jar mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] classlist: 888.08 ms, 0.96 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (cap): 461.21 ms, 0.96 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] setup: 1,674.56 ms, 0.96 GB
21:04:16,716 INFO [org.jbo.threads] JBoss Threads version 3.4.2.Final
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (clinit): 266.48 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (typeflow): 7,152.50 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (objects): 9,486.04 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (features): 518.46 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] analysis: 18,004.64 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] universe: 758.33 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (parse): 1,882.04 ms, 3.22 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (inline): 2,659.86 ms, 5.29 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] (compile): 14,088.48 ms, 5.54 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] compile: 19,952.01 ms, 5.54 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] image: 2,717.06 ms, 5.54 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] write: 321.33 ms, 5.54 GB
[mvn-repo-cleaner-1.0.0-SNAPSHOT-runner:20610] [total]: 44,515.50 ms, 5.54 GB
# Printing build artifacts to: /home/jdlee/src/personal/mvn-repo-cleaner/target/mvn-repo-cleaner-1.0.0-SNAPSHOT-native-image-source-jar/mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.build_artifacts.txt
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] objcopy --strip-debug mvn-repo-cleaner-1.0.0-SNAPSHOT-runner
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 45883ms
$ ls -alh target/
total 38M
drwxrwxr-x 1 jdlee jdlee 604 Oct 10 21:04 .
drwx--x--x. 1 jdlee jdlee 244 Oct 10 21:02 ..
drwxrwxr-x 1 jdlee jdlee 72 Oct 10 20:56 classes
drwxrwxr-x 1 jdlee jdlee 22 Oct 10 20:56 generated-sources
drwxrwxr-x 1 jdlee jdlee 28 Oct 10 20:56 maven-archiver
drwxrwxr-x 1 jdlee jdlee 42 Oct 10 20:56 maven-status
-rw-rw-r-- 1 jdlee jdlee 16K Oct 10 20:59 mvn-repo-cleaner-1.0.0-SNAPSHOT.jar
-rw-rw-r-- 1 jdlee jdlee 16K Oct 10 20:56 mvn-repo-cleaner-1.0.0-SNAPSHOT.jar.original
drwxrwxr-x 1 jdlee jdlee 206 Oct 10 21:04 mvn-repo-cleaner-1.0.0-SNAPSHOT-native-image-source-jar
-rwxrwxr-x 1 jdlee jdlee 34M Oct 10 21:04 mvn-repo-cleaner-1.0.0-SNAPSHOT-runner
-rw-rw-r-- 1 jdlee jdlee 4.0M Oct 10 20:56 mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar
drwxrwxr-x 1 jdlee jdlee 112 Oct 10 21:04 quarkus-app
-rw-rw-r-- 1 jdlee jdlee 293 Oct 10 21:04 quarkus-artifact.properties
That gives us a 34M file, mvn-repo-cleaner-1.0.0-SNAPSHOT-runner
, with an amazing startup:
1
2
3
4
5
6
7
8
9
10
$ time java -jar target/mvn-repo-cleaner-1.0.0-SNAPSHOT-runner.jar --help
...
real 0m0.787s
user 0m1.130s
sys 0m0.101s
$ time ./target/mvn-repo-cleaner-1.0.0-SNAPSHOT-runner --help
...
real 0m0.034s
user 0m0.020s
sys 0m0.015s
From 787 thousandths of a second to 34 thousandths of a second. That’s pretty impressive!
With that, we have a complete command line utility based on Quarkus (with a big tip o' the hat to techpavan for doing the hard work of making the original utility). Hopefully, this will serve as a nice example to follow if and when you write your own utility.
You can find the complete source here. Use it in good health. :)