Javac and Java Katas, Part 1: Class Path

Here, I'd like to talk you through three Java katas, ranging from the simplest to the most complex. These exercises should help you gain experience working with JDK tools such as javac, java, and jar. By doing them, you'll get a good understanding of what goes on behind the scenes of your favorite IDE or build tools like Maven, Gradle, etc.

None of this denies the benefits of an IDE. But to be truly skilled at your craft, understand your essential tools and don’t let them get rusty.
- Gail Ollis, "Don’t hIDE Your Tools"

Getting Started

The source code can be found in the GitHub repository. All commands in the exercises below are executed inside a Docker container to avoid any particularities related to a specific environment.

Thus, to get started, clone the repository and run the command below from its java-javac-kata folder:

Shell
 
docker run --rm -it --name java_kata -v .:/java-javac-kata --entrypoint /bin/bash maven:3.9.6-amazoncorretto-17-debian


Kata 1: "Hello, World!" Warm Up

In this kata, we will be dealing with a simple Java application without any third-party dependencies. Let's navigate to the /class-path-part/kata-one-hello-world-warm-up folder and have a look at the directory structure.

/class-path-part/kata-one-hello-world-warm-up folder directory structure

Within this directory, we can see the Java project structure and two classes in the com.example.kata.one package.

Compilation

Shell
 
javac -d ./target/classes $(find -name '*.java')


Compiled Java classes appearing in the target/classes folder

The compiled Java classes should appear in the target/classes folder, as shown in the screenshot above. Try using the verbose option to see more details about the compilation process in the console output:

Shell
 
javac -verbose -d ./target/classes $(find -name '*.java')


With that covered, let's jump into the execution part.

Execution

Shell
 
java --class-path "./target/classes" com.example.kata.one.Main


As a result, you should see Hello World! in your console. 

Try using different verbose:[class|gc|jni] options to get more details on the execution process:

Shell
 
java -verbose:class --class-path "./target/classes" com.example.kata.one.Main


As an extra step, it's worth trying to remove classes or rename packages to see what happens during both the complication and execution stages. This will give you a better understanding of which problems result in particular errors.

Packaging

Building Jar

Shell
 
jar --create --file ./target/hello-world-warm-up.jar -C target/classes/ .


Built jar placed in the target folder

The built jar is placed in the target folder. Don't forget to use the verbose option as well to see more details:

Shell
 
jar --verbose --create --file ./target/hello-world-warm-up.jar -C target/classes/ .


You can view the structure of the built jar using the following command:

Shell
 
jar -tf ./target/hello-world-warm-up.jar


With that, let's proceed to run it:

Shell
 
java --class-path "./target/hello-world-warm-up.jar" com.example.kata.one.Main


Building Executable Jar

To build an executable jar, the main-class must be specified:

Shell
 
jar --create --file ./target/hello-world-warm-up.jar --main-class=com.example.kata.one.Main -C target/classes/ .


It can then be run via jar option:

Shell
 
java -jar ./target/hello-world-warm-up.jar


Kata 2: Third-Party Dependency

In this kata, you will follow the same steps as in the previous one. The main difference is that our Hello World! application uses guava-30.1-jre.jar as a third-party dependency. Also, remember to use the verbose option to get more details.

So, without further ado, let's get to the /class-path-part/kata-two-third-party-dependency folder and check out the directory's structure.

/class-path-part/kata-two-third-party-dependency folder directory structure

Compilation

Shell
 
javac --class-path "./lib/*" -d ./target/classes/ $(find -name '*.java')


The class-path option is used to specify the path to the lib folder where our dependency is stored.

Execution

Shell
 
java --class-path "./target/classes:./lib/*" com.example.kata.two.Main


Packaging

Building Jar

Shell
 
jar --create --file ./target/third-party-dependency.jar -C target/classes/ .


And let us run it:

Shell
 
java --class-path "./target/third-party-dependency.jar:./lib/*" com.example.kata.two.Main


Building Executable Jar

Our first step here is to create a MANIFEST.FM file with the Class-Path specified:

Shell
 
echo 'Class-Path: ../lib/guava-30.1-jre.jar' > ./target/MANIFEST.FM


Next up, we build a jar with the provided manifest option:

Shell
 
jar --create \
    --file ./target/third-party-dependency.jar \
    --main-class=com.example.kata.two.Main \
    --manifest=./target/MANIFEST.FM \
    -C target/classes/ .


Finally, we execute it:

Shell
 
java -jar ./target/third-party-dependency.jar


Building Fat Jar

First of all, we need to unpack our guava-30.1-jre.jar into the ./target/classes/ folder (be patient, this can take some time):

Shell
 
cp lib/guava-30.1-jre.jar ./target/classes && \
cd ./target/classes && \
jar xf guava-30.1-jre.jar && \
rm ./guava-30.1-jre.jar && \
rm -r ./META-INF && \
cd ../../


With all the necessary classes in the ./target/classes folder, we can build our fat jar (again, be patient as this can take some time):

Shell
 
jar --create --file ./target/third-party-dependency-fat.jar --main-class=com.example.kata.two.Main -C target/classes/ .


Now,  we can run our built jar:

Shell
 
java -jar ./target/third-party-dependency-fat.jar


Kata 3: Spring Boot Application Conquest

In the /class-path-part/kata-three-spring-boot-app-conquest folder, you will find a Maven project for a simple Spring Boot application. The main goal here is to apply everything that we have learned so far to manage all its dependencies and run the application, including its test code.

As a starting point, let's run the following command:

Shell
 
mvn clean package && \
find ./target/ -mindepth 1 ! -regex '^./target/lib\(/.*\)?' -delete


This will leave only the source code and download all necessary dependencies into the ./target/lib folder.

Download all necessary dependencies into the ./target/lib folderCompilation

Shell
 
javac --class-path "./target/lib/compile/*" -d ./target/classes/ $(find -P ./src/main/ -name '*.java')


Execution

Shell
 
java --class-path "./target/classes:./target/lib/compile/*" com.example.kata.three.Main


As an extra step for both complication and execution, you can try specifying all necessary dependencies explicitly in the class-path. This will help you understand that not all artifacts in the ./target/lib/compile are needed to do that.

Packaging

Let's package our compiled code as a jar and try to run it. 

It won't be a Spring Boot jar because Spring Boot uses a non-standard approach to build fat jars, including its own class loader. See the documentation on The Executable Jar Format for more details. In this exercise, we will package our source code as we did before to demonstrate that everything can work in the same way with Spring Boot, too.

Shell
 
jar --create --file ./target/spring-boot-app-conquest.jar -C target/classes/ .


Now, let's run it to verify that it works:

Shell
 
java --class-path "./target/spring-boot-app-conquest.jar:./target/lib/compile/*" com.example.kata.three.Main


Test Compilation

Shell
 
javac --class-path "./target/classes:./target/lib/test/*:./target/lib/compile/*" -d ./target/test-classes/ $(find -P ./src/test/ -name '*.java')

Take notice that this time we are searching for source files in the ./src/test/ directory, and both the application source code and test dependencies are added to the class-path.

Test Execution

To be able to run code via java, we need an entry point (a class with the main method). Traditionally, tests are run via a Maven plugin or by an IDE, which have their own launchers to make this process comfortable for developers. To demonstrate test execution, the junit-platform-console-standalone dependency, which includes the org.junit.platform.console.ConsoleLauncher with the main method, is added to our pom.xml. Its artifact can also be seen in the ./target/lib/test/* folder.

Shell
 
java --class-path "./target/classes:./target/test-classes:./target/lib/compile/*:./target/lib/test/*" \
     org.junit.platform.console.ConsoleLauncher execute --scan-classpath --disable-ansi-colors


Wrapping Up

Gail's article, "Don’t hIDE Your Tools" quoted at the very beginning of this article, taken from 97 Things Every Java Programmer Should Know by Kevlin Henney and Trisha Gee, inspired me to start thinking in this direction and eventually led to the creation of this post.

Hopefully, by doing these katas and not just reading them, you have developed a better understanding of how the essential JDK tools work.

 

 

 

 

Top