How to build native CLI apps using Java, Maven, GraalVM, Picocli, JReleaser and GitHub actions

Rajesh Rajagopalan

Rajesh Rajagopalan

·

Follow

Published in

PeerIslands Engineering Blogs

·

7 min read

·

Jan 11, 2022

19

2

A reference implementation that you can use as a jump start project

I have seen several traditional Unix command-line tools being re-written in Rust recently. Being a fan of Java, I started looking at the tooling available to build cross-platform Command Line applications in Java. Based on my research and learning, I see the following toolset being a great combination to quickly build CLI tools that are lean and performant. In the process, I built a JWT CLI utility using

In addition, I built integrations with the following to provide a view of code quality and related metrics that are essential for code that is maintainable.

GitHub Repo: https://github.com/rrajesh1979/ref-java-jwt

In this blog, I detail my learnings and how you can go about building your own.

Building JWT CLI

I use IntelliJ for development. You can use your favourite IDE.

Start by setting up a new Maven project

IntelliJ uses the bundled Maven by default. If you have a more recent version of Maven installed, you can configure the same.

Install Maven Wrapper

Maven wrapper is an excellent utility that makes it easy to build our code on any machine including CI/CD servers. And we can enforce usage of specific Maven vesion.

https://engineering.peerislands.io/media/2ea925300d0c1df7833e83c7367957e7

Add LICENSE file

Add a new file in your GitHub repo. Name it as LICENSE. GitHub will prompt you to select a license template.

Configure Maven to include the License file in your packaged application. And to validate all your source code includes a Copyright header.

https://engineering.peerislands.io/media/92c411ba386795d1b4efc9b79fd36c5a

Add .gitignore file

Here is what I have used as a common template across projects.

https://engineering.peerislands.io/media/43fff9f05a9bedb062f71b0adbd6a715

Here is the core Java program to Encode and Decode JWT. I know there are more efficient ways to do this. I have focussed on the overall tooling to build a CLI application.

The JWTUtil has two main methods — createJWT and decodeJWT. And I have used JJWT — a pure Java library that simplifies creating and verifying JSON Web Tokens (JWTs) on the JVM.

https://engineering.peerislands.io/media/209a08335888e60755608b93a683bb95

Picocli — to efficiently build a JWT CLI app

I then used Picocli to build the CLI traits.

Picocli aims to be the easiest way to create rich command line applications that can run on and off the JVM. You can build Java command line applications with almost zero code. It supports a variety of command line syntax styles including POSIX, GNU, MS-DOS and more. It generates highly customizable usage help messages that use ANSI colors and styles to contrast important elements and reduce the cognitive load on the user. Picocli-based applications can have command line TAB completion showing available options, option parameters and subcommands, for any level of nested subcommands. Finally, Picocli generates beautiful documentation for your application (HTML, PDF and Unix man pages).

With a few lines of code, you can build a CLI app that behaves as shown below.

I segregated the code into 3 files to keep it maintainable — one for the main command and one for each of the sub-commands to encode and decode.

JWTC is the main Command class.

https://engineering.peerislands.io/media/e51b053d35a41d95621041bf0faa1931

JWTCEncode and JWTCDecode are the sub-command implementations.

https://engineering.peerislands.io/media/5280e1992103fa123c1d63774cec5f2a
https://engineering.peerislands.io/media/156d6f72cf714dbfe5cc73419fada69d

GraalVM Native Image

While this gives me a good command line utility built using Java and Picocli, Java applications are generally hard to distribute. You either end up requiring JVM installed in the target system. Or end up with a significantly heavy deployment package. Running inside a JVM comes with startup and footprint costs.

GraalVM solves these problems with its native images for existing JVM-based applications. The native image generation process employs static analysis to find any code reachable from the main Java method and then performs full ahead-of-time (AOT) compilation. The resulting native binary contains the whole program in machine code form for its immediate execution that is faster and leaner requiring fewer compute resources.

Maven profile to build native image using GraalVM native-image-plugin is shown below.

https://engineering.peerislands.io/media/0ed3988616bcc355a754cab321fe6d99

The following command converts the existing application into a native image.

https://engineering.peerislands.io/media/794cc95cfb6d5b0673cd1e19ec2895b8
https://engineering.peerislands.io/media/c0e2035f3a2d3d8c13de92ffb45d07cb

Native Image has partial support for reflection and needs to know ahead-of-time the reflectively accessed program elements.

Native Image tries to resolve the target elements through a static analysis that detects calls to the Reflection API. Where the analysis fails, the program elements reflectively accessed at runtime must be specified using a manual configuration.

Picocli-based applications can be ahead-of-time compiled to a native image, with extremely fast startup time and lower memory requirements, which can be distributed as a single executable file. The picocli-codegen module contains an annotation processor that generates the necessary configuration files under META-INF/native-image/picocli-generated/$project during compilation, to be included in the application jar. By embedding these configuration files, your jar is instantly Graal-enabled: the native-image tool used to generate the native executable can get the configuration from the jar. In most cases, no further configuration is needed when generating a native image.

For other libraries, we need to configure manually. For example, the JJWT library that I used, requires manual configuration. Here is a simple approach that I learnt.

Build the native-image using the following Maven configuration

<buildArg>--allow-incomplete-classpath</buildArg>

When you run the resulting application, the application will report the Class files it is unable to find — as shown below.

https://engineering.peerislands.io/media/f48e6c75fdf1b5e216ae265f966b1676

You can then add them to the reflect-config.json in the resources/META-INF/native-image folder of your project.

And you specify the configuration in native-image.properties for GraalVM native-image-plugin to refer the reflect-config.json while building the native image.

-H=ReflectionConfigurationResources=${.}/reflect-config.json

My reflect-config.json looks as follows.

https://engineering.peerislands.io/media/591cdf7d4a36ede651158b3ac0a9ad70

Note: For additional performance, native images can be built with profile guided optimizations gathered in a previous run of the application.

Cross platform releases with JReleaser and Maven Assembly plugin

Once you have native image available, we need to package and distribute it in respective platforms and stores.

I have used JReleaser along with Maven Assembly plugin to achieve this.

The Assembly Plugin for Maven enables developers to combine project output into a single distributable archive that also contains dependencies, modules, site documentation, and other files.

The Maven Assembly plugin configuration I used is below.

https://engineering.peerislands.io/media/a1e345089d3ad3f9ac4f266a6163f791

I then built operating system specific distributions using the following profile.

https://engineering.peerislands.io/media/d15b814a2e883b82ed8ab1f8c995d2f0

Finally, I used JReleaser to package and release to GitHub, HomeBrew. JReleaser also supports SKDMAN, JBang and many others.

The jreleaser-maven-plugin configuration and jreleaser.yml configuration I used are below.

https://engineering.peerislands.io/media/b318df43a9885d946a12eaa3af0a21a3
https://engineering.peerislands.io/media/5e79bd1660d967a1d34d16b38c61060c

Automating your CI/CD pipeline using GitHub actions

Now the final piece in the puzzle. How can you set up a CI/CD pipeline that automates the complete process above?

GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. You can build, test, and deploy your code right from GitHub. You can kick off workflows with GitHub events like push to a branch. Hosted runners for every major OS make it easy to build and test all your projects.

I used the following GitHub workflow configuration

https://engineering.peerislands.io/media/6b12c37a4dcafbdcd28ce5f4ae74f1e8

Installation and usage

Now that you have completed the release, time to test the CLI utility. I have a Mac. And this is how I can install and test the utility.

https://engineering.peerislands.io/media/6d8e5b92f6a12236928bc160b1c51c3c

Performance

For CLI applications, performance is paramount. And GraalVM native images provide significantly better performance and startup times. GraalVM takes over the compilation of Java bytecode to machine code. The compiler of GraalVM provides performance advantages for highly abstracted programs due to its ability to remove costly object allocations in many scenarios.

A simple comparison of the JWT encode using GraalVM vs traditional JAR is shown below.

https://engineering.peerislands.io/media/748718ef84f951600e63d17f0869229b

Closing thoughts

While there is a lot of upfront configuration involved, once you set this right with all the tooling, it will significantly improve your productivity to build CLI tools.

Better yet, you can fork my repo to build your own CLI application.

Look forward to hearing your experience as well and feedback.

References

Graalvm

Picocli

Jreleaser

Java Maven

Github Actions

19

2

Rajesh Rajagopalan

Written by Rajesh Rajagopalan

77 Followers

·Editor for

PeerIslands Engineering Blogs

Learner for life

Follow

More from Rajesh Rajagopalan and PeerIslands Engineering Blogs

Reshaping Software Development powered by Peer.AI

Rajesh Rajagopalan

Rajesh Rajagopalan

in

PeerIslands Engineering Blogs

Reshaping Software Development powered by Peer.AI

Future of Software Development powered by AI Agents

4 min read·Apr 23, 2024

4

MongoDB Authentication : AWS IAM

Rajesh Vinayagam

Rajesh Vinayagam

in

PeerIslands Engineering Blogs

MongoDB Authentication : AWS IAM

When using MongoDB on AWS, it is recommended to use AWS IAM to manage access to MongoDB resources. This can be done through the use of IAM…

5 min read·Mar 14, 2023

1

DocumenDB 2 Mongo : Migration Approach

Rajesh Vinayagam

Rajesh Vinayagam

in

PeerIslands Engineering Blogs

DocumenDB 2 Mongo : Migration Approach

There are several reasons why a user might consider migrating from Amazon DocumentDB to MongoDB Atlas.

10 min read·Feb 5, 2023

3

1

Extending OpenAI GPT-4 using LangChain and Pinecone for Q&A over your own content using

Rajesh Rajagopalan

Rajesh Rajagopalan

in

PeerIslands Engineering Blogs

Extending OpenAI GPT-4 using LangChain and Pinecone for Q&A over your own content using

Using LangChain to orchestrate process to get answer for a user query from OpenAI GPT-4 using in-context learning

7 min read·May 1, 2023

28

1

See all from Rajesh Rajagopalan

See all from PeerIslands Engineering Blogs

Recommended from Medium

How an empty S3 bucket can make your AWS bill explode

Maciej Pocwierz

Maciej Pocwierz

How an empty S3 bucket can make your AWS bill explode

Imagine you create an empty, private AWS S3 bucket in a region of your preference. What will your AWS bill be the next morning?

4 min read·Apr 30, 2024

10.1K

124

The beginning of the end for Terraform?

Alistair Grew

Alistair Grew

in

Netpremacy Global Services

The beginning of the end for Terraform?

Entering the graveyard of good software that is IBM…

5 min read·Apr 26, 2024

802

20

Lists

Two square white speech bubbles on a bright pink background.

Staff Picks

636 stories·953 saves

Stories to Help You Level-Up at Work

19 stories·601 saves

Self-Improvement 101

20 stories·1760 saves

Productivity 101

20 stories·1624 saves

15 + 11 Mistakes Every Java Developer MUST avoid TODAY

Varsha Das

Varsha Das

in

Level Up Coding

15 + 11 Mistakes Every Java Developer MUST avoid TODAY

Do you want to be your code reviewer’s favorite developer?

7 min read·May 1, 2024

314

9

It’s Time to Retire Terraform

Eric Larssen

Eric Larssen

in

Real Kinetic Blog

It’s Time to Retire Terraform

Terraform exists in many people’s hearts much like a friend or a loved one, or maybe even an enemy. Whether it’s your job to maintain the…

10 min read·Apr 23, 2024

637

40

Google Lays Off Flutter and Dart Teams Following Python Team Cuts

Meng Li

Meng Li

in

The Pythoneers

Google Lays Off Flutter and Dart Teams Following Python Team Cuts

The Truth Behind Google’s Mass Layoffs: How Tech Giants Adapt in the Era of AI

·4 min read·May 1, 2024

409

8

Github Copilot vs Amazon CodeWhisperer

Anil Goyal

Anil Goyal

Github Copilot vs Amazon CodeWhisperer

Both Github Copilot and Amazon Code Whisperer are AI coding assistant tool i.e., an AI-powered tool that has been designed to help us…

5 min read·Jan 18, 2024

16

See more recommendations