Featured image of post Java Compiling and Packaging Basics

Java Compiling and Packaging Basics

A Java program

Compiling and running a Java program in the command line is easy. Let’s start with the most basic application: Hello World. First, we create a HelloWorld.java file.

public class HelloWorld {

    public static void main(String[] args) {

It can be compiled to a HelloWorld.class file with the following command:

$ javac HelloWorld.java

We can then run the .class file using the java command.

$ java HelloWorld
Hello World

Simple and easy.

A Java program with dependencies

What if we want to use a third-party package for our application?

We will be using the org.apache.commons.commons-lang3 package, so download it here and put it into the lib folder of the project. The folder structure should look like the following:


Let’s modify HelloWorld.java a little bit.

import org.apache.commons.lang3.StringUtils;

class HelloWorld {
    public static void main(String[] args) {
        System.out.println(StringUtils.capitalize("hello world"));

This time, we need to use the -classpath argument when compiling our program.

$ javac -classpath lib/commons-lang3-3.12.0.jar HelloWorld.java

The -classpath argument adds commons-lang3-3.12.0.jar to the classpath so that when we use the import org.apache.commons.lang3.StringUtils; statement in HelloWorld.java, the Java compiler is able to find the StringUtils.class file, which is located under org.apache.commons.lang3 in commons-lang3-3.12.0.jar.

Then, how to run HelloWorld.class? First, let’s try java HelloWorld.

$ java HelloWorld  

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
        at HelloWorld.main(HelloWorld.java:5)

Java is complaining that it couldn’t find StringUtils. So let’s add commons-lang3-3.12.0.jar to classpath.

$ java -classpath lib/commons-lang3-3.12.0.jar HelloWorld

Error: Could not find or load main class HelloWorld
Caused by: java.lang.ClassNotFoundException: HelloWorld

It turns out that when not specifying -classpath, it has a default value: the current working directory. That is why we can run java HelloWorld directly when there are no dependencies: Java can find the HelloWorld.class file in the default classpath. However, now we are overriding the classpath value, so we should manually add the current directory.

$ java -classpath lib/commons-lang3-3.12.0.jar:./ HelloWorld

Hello world

This time it works!

The takeaway is that both the Java compiler (javac) and the Java virtual machine (java) need a classpath, which specifies where to load the classes. Classpath is an array separated by :, and the elements could be either local filesystem paths or .jar files. Third-party packages imported into our program should be added to classpath to support compilation and execution.

Dependency scopes

Like the commons-lang3 package we used above, most packages should be added to both compile-time classpath (javac command) and runtime classpath (java command). However, in real life, things can get much more complicated. For example, when we build an application to be deployed to a servlet container (e.g., Tomcat), the servlet API package should be added to classpath at compile-time, but should not be added to runtime classpath since the servlet container will provide the API. Another example is the JDBC connector packages (for example, org.postgresql:postgresql), which are not needed during compile time but are required at runtime. What makes the situation even more complicated is testing. The third-party packages supporting unit testing (like JUnit) should not be included in classpath when we compile and run the application, but should be included when we compile and run the tests.

Such complexity leads to the concept of dependency scopes in build tools like Maven and Gradle. The table below summarizes the commonly-used dependency scopes and their effects on classpath in Maven and Gradle.

scope name (Maven) scope name (Gradle) classpath example
compile (default) implementation compile time and runtime most packages
provided compileOnly compile time servlet APIs
runtime runtimeOnly runtime JDBC connectors
test testImplementation compile time and runtime (test only) JUnit

An example of those different scopes is shown in the spring-framework-petclinic project, which is built using the plain old Spring framework (i.e. no Spring Boot), and is meant to be packaged as a war archive and deployed to Tomcat. We see that Tomcat packages are added to compile-time classpath only since at runtime the Tomcat container will provide them. In addition, The MySQL driver is listed as a runtime dependency. Finally, Spring test is a test dependency since it’s only required during testing.

Packaging a Java program

Let’s go back to our HelloWorld. We can use the jar command to create a Jar archive out of HelloWorld.class.

jar -cf hello-world.jar HelloWorld.class

The resulting Jar file, hello-world.jar, contains the HelloWorld.class file and a manifest file named MANIFEST.MF in the META-INF folder, which includes the metadata about this Jar archive.


Note that the Jar archive doesn’t have anything from the commons-lang3 package. Therefore, we call it a thin Jar, which are Jars containing only our own classes and not any third-party classes.

Let’s take a look at the MANIFEST.MF file. For our simple example, It contains only the most basic information, like the manifest version and the Java compiler information.

Manifest-Version: 1.0
Created-By: 11.0.13 (Eclipse Adoptium)

It does not have the Main-Class attribute so this hello-world.jar is not executable, which means we cannot execute it using java -jar hello-world.jar. However, we can execute it in the traditional way – supplying -classpath.

$ java -classpath lib/commons-lang3-3.12.0.jar:hello-world.jar HelloWorld

Hello world

We can add the Main-Class attribute to MANIFEST.MF and make it executable. Another attribute, Class-Path, is also needed since we are using third-party dependencies. To do so, let’s create a new file manifest-overrides.txt.


We put the following to it:

Main-Class: HelloWorld
Class-Path: lib/commons-lang3-3.12.0.jar

Warning: The text file must end with a new line or carriage return. The last line will not be parsed properly if it does not end with a new line or carriage return. (reference)

Then we create a Jar archive and use the manifest-overrides.txt as the modification file.

$ jar -cfm hello-world.jar manifest-overrides.txt HelloWorld.class

Then make sure that the META-INF/MANIFEST.MF file in the Jar contains Main-Class and Class-Path.

Manifest-Version: 1.0
Main-Class: HelloWorld
Class-Path: lib/commons-lang3-3.12.0.jar
Created-By: 11.0.13 (Eclipse Adoptium)

Now we execute our hello-world.jar.

$ java -jar hello-world.jar
Hello world

It works! Such a Jar file is called an executable Jar since it can be executed by the java -jar command directly, without specifying the main class and the classpath at the command line.

Fat Jars

However, if we move hello-world.jar to another location, executing java -jar hello-world.jar will fail. The reason is that the Class-Path attribute only adds the listed Jar archives to Java classpath, it does not magically provide them. Therefore, the Java virtual machine still expects the lib/commons-lang3-3.12.0.jar file in our local filesystem.

This is not good when deploying an application (e.g., a website), since we have to provide all the supporting Jars for the application to run on our servers. That is why people come up with the idea of fat Jars: Jar archives that contain our classes and third-party classes.

We can create a fat Jar for our HelloWorld application manually. To do so, we need to:

  • Extract everything from commons-lang3-3.12.0.jar (excluding the META-INF folder).
  • Put the extracted files into hello-world.jar
  • Modify the META-INF/MANIFEST.MF in hello-world.jar and remove the Class-Path attribute. It’s not needed anymore.

Below is the resulting hello-world.jar. It’s significantly larger than our previous one, since now it includes our HelloWorld.class and the org.apache.commons.lang3 package from commons-lang3-3.12.0.jar.


Fat Jar archives can always be executed via java -jar hello-world.jar without supporting Jars. Therefore, they make it easy to deploy and distribute our applications. In practice, we build fat Jars with the help of Maven or Gradle. For example, the Spring Boot Gradle plugin provides a bootJar task which builds the whole application as a fat Jar. It works differently than the manual process I mentioned above. Still, the basic principle is similar: put all the third-party packages added to the runtime classpath (i.e. implementation scope + runtimeOnly scope) to the resulting Jar archive.

What is interesting about fat Jars is the developmentOnly dependency scope introduced by Spring Boot Gradle plugin. This scope is provided for the spring-boot-devtools package, which facilitates local development by enabling auto-restart and live reload. The developmentOnly scope is similar to the runtimeOnly scope we mentioned above. However, the package is only added to the runtime classpath but not to the resulting fat Jar. This scope is perfectly suitable for spring-boot-devtools since it’s not needed when the application runs in production.