This blog post is about the fundamentals of Java Maven. The focus is on the manually steps, from the creation of a project, to the package of the application. I also provide a multistage Dockerfile, that combines the these steps.
Java Overview #
Java Language #
- Object-oriented programming language that can run on many operating systems without modification.
Core Components:
-
Java Virtual Machine (JVM): Executes Java programs, providing a platform-independent runtime.
-
Java Development Kit (JDK): A toolkit for developing Java applications (includes the compiler, libraries, and JVM).
-
Java Runtime Environment (JRE): A subset of the JDK that includes only what’s needed to run Java applications.
OpenJDK #
-
Open-source version of Java SE (Java Platform Standard Edition)
-
Alternative to the official Java implementation by Oracle, which has a more restrictive license.
OpenJDK Components:
-
Java Developer Kit (JDK): Includes the Javac compiler, used for compiling Java into bytecode. Necessary to create stand-alone Java executables.
-
Java Runtime Environment (JRE): Executes Java code and compiled Java bytecode.
-
To only run Java programs, JRE can be used without the JDK.
Maven #
-
Build automation tool and dependency management tool for Java projects. Simplifies and standardizes the build process of Java applications.
-
Compiles source code into executable code like
.class
or.jar
files -
Maven automatically downloads and manages external libraries / dependencies from a
pom.xml
configuration file, that are needed for the project. -
Provides a standardized project structure (e.g., where to place source code, resources and tests).
Java Setup (Linux Server) #
Java Development Kit (JDK) Installation #
# Find the latest OpenJDK version
https://jdk.java.net/
# Install OpenJDK version 17
sudo apt install openjdk-17-jdk -y
# Optional, install only the JRE component of OpenJDK
sudo apt install openjdk-17-jre -y
Verify Java installation #
# Verify installation / check version
java -version
# Shell output:
openjdk version "17.0.13" 2024-10-15
OpenJDK Runtime Environment (build 17.0.13+11-Ubuntu-2ubuntu124.04)
OpenJDK 64-Bit Server VM (build 17.0.13+11-Ubuntu-2ubuntu124.04, mixed mode, sharing)
# Verify Java compiler / check version (Only with JDK installation, not with JRE only)
javac -version
# Shell output:
javac 17.0.13
Set Java Environment Variables #
Current user:
# Append .bashrc of the current user
echo -e 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))' >> ~/.bashrc
echo -e 'export PATH=$PATH:$JAVA_HOME/bin' >> ~/.bashrc
# Apply changes
source ~/.bashrc
# Verify environemtn variables
echo $JAVA_HOME
echo $PATH
# Shell output:
/usr/lib/jvm/java-17-openjdk-amd64
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/lib/jvm/java-17-openjdk-amd64/bin
Java Maven Setup (Linux Server) #
- Java Development Kit (JDK) must be installed
Maven Installation #
# Install Maven
sudo apt install maven -y
Verify Installation #
# Verify the Installation
mvn -version
# Shell output:
Apache Maven 3.8.7
Maven home: /usr/share/maven
Java version: 17.0.13, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-49-generic", arch: "amd64", family: "unix"
Maven Commands Overview #
# Clean up the "target/" directory
mvn clean
# Verify the "pom.xml" is correct
mvn validate
# Compile the source code
mvn compile
# Run unit tests
mvn test
# Package the compiled code into a JAR or WAR
mvn package
Java Example Application #
Create Application #
# Create a simple hello world application
vi HelloWorld.java
// Define public class named 'HelloWorld'. The class name must match the file name (HelloWorld.java).
public class HelloWorld {
// Entry point of the Java application
public static void main(String[] args) {
// Standard output stream, print sting to the console
System.out.println("Hi there, Java test");
}
}
Run Application Using the (OpenJDK) JRE #
Use the previously installed(OpenJDK) JRE to run the Java code:
# Run a Java Application
java HelloWorld.java
# Shell output:
Hi there, Java test
Compile & Run Application Using the JDK / JRE #
Compile Java code into Java bytecode:
# Compile with Javac compiler
javac HelloWorld.java
# Run the compiled class with JRE
java HelloWorld
# Shell output:
Hi there, Java test
Java Maven Example Application #
Create Project #
# Create a new Maven project: Standard Maven project structure
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=HelloMaven-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.5 -DinteractiveMode=false
-
DgroupId
Unique identifier for the project, typically your domain name in reverse -
-DartifactId
The name of your project, for exampleHelloMaven-app
Verify Project Structure #
# Navigate into the project
cd HelloMaven-app
HelloMaven-app
├── pom.xml # Maven configuration file
└── src
├── main
│ └── java
│ └── com
│ └── mycompany
│ └── app
│ └── App.java # Main Java file
└── test
└── java
└── com
└── mycompany
└── app
└── AppTest.java # Test file
pom.xml
Core of a project configuration in Maven.
Project Files #
Main Java File #
# Open the main Java file "App.java"
vi src/main/java/com/mycompany/app/App.java
// Define the package name for this class: A package is a namespace that organizes related classes and interfaces
package com.mycompany.app;
// Import the DateTime class from the Joda-Time library
import org.joda.time.DateTime;
// Define a public class named "App"
public class App {
// Print some text
public static void main(String[] args) {
System.out.println("Hello, from Maven. Time:");
// Use Joda-Time to get and print the current date and time
DateTime currentTime = new DateTime();
System.out.println(currentTime.toString("yyyy-MM-dd HH:mm:ss"));
}
}
Add Dependencies to pom.xml #
Add the Joda and Junit test dependencies to pom.xml
:
# Open "pom.xml"
vi pom.xml
The original “dependency” configuration looks like this:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<!-- Optionally: parameterized tests support -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Add the Joda and Junit test dependencies:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<!-- Optionally: parameterized tests support -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
# Add Joda & Junit test dependency
<!-- Joda-Time dependency -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.14</version>
</dependency>
<!-- Junit dependency -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
Add Plugins to pom.xml #
-
The default behavior of Maven does not include dependencies in the packaged JAR file (thin JAR).
-
Add the
maven-assembly-plugin
topom.xml
bundle the dependencies into the final JAR, creating a fat JAR that can run without needing external dependencies. -
This plugin simplifies deployment by producing a standalone, self-contained JAR file
# Open "pom.xml"
vi pom.xml
The original “plugins” sections looks like this:
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.3.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.6.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
Add the maven-assembly-plugin:
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.3.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.6.1</version>
</plugin>
<!-- Add maven-assembly-plugin -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.mycompany.app.App</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
Adapt Unit Test #
# Adopt the Test
vi src/test/java/com/mycompany/app/AppTest.java
package com.mycompany.app;
import static org.junit.Assert.assertTrue;
import org.joda.time.DateTime;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
public class AppTest {
@Test
public void testMainOutput() {
// Capture the console output
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
// Run the main method
App.main(new String[]{});
// Verify the output contains the greeting
String output = outContent.toString();
assertTrue("Output should contain the greeting", output.contains("Hello, from Maven. Time:"));
// Verify the output contains a correctly formatted timestamp
DateTime now = new DateTime();
String expectedDate = now.toString("yyyy-MM-dd");
assertTrue("Output should contain the current date", output.contains(expectedDate));
// Reset the console output
System.setOut(System.out);
}
}
Build the Example project #
Run the Unit Test #
# Run the test
mvn test
# Shell output:
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.mycompany.app:HelloMaven-app >------------------
[INFO] Building HelloMaven-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ HelloMaven-app ---
[INFO] skip non existing resourceDirectory /home/ubuntu/HelloMaven-app/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ HelloMaven-app ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 17] to target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:testResources (default-testResources) @ HelloMaven-app ---
[INFO] skip non existing resourceDirectory /home/ubuntu/HelloMaven-app/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:testCompile (default-testCompile) @ HelloMaven-app ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 17] to target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:3.3.0:test (default-test) @ HelloMaven-app ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mycompany.app.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.022 s -- in com.mycompany.app.AppTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.815 s
[INFO] Finished at: 2024-11-26T21:10:24Z
[INFO] ------------------------------------------------------------------------
Run the Application Without Compiling #
Run the application without without manually compiling it (Direct Execution). Maven handles the build and execution in one step:
# Run the application
mvn exec:java -Dexec.mainClass="com.mycompany.app.App"
# Shell output:
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.mycompany.app:HelloMaven-app >------------------
[INFO] Building HelloMaven-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:3.5.0:java (default-cli) @ HelloMaven-app ---
Hello, from Maven. Time:
2024-11-26 21:20:03
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.214 s
[INFO] Finished at: 2024-11-26T21:20:03Z
[INFO] ------------------------------------------------------------------------
-
mvn exec:java
Runs the exec plugin to execute the application. -
-Dexec.mainClass="com.mycompany.app.App"
Specifies the fully qualified name of the main class.
Build the Application: Without Dependencies #
Compile the Application #
Compile the Java source code of the project into Java bytecode (.class
files) that can be executed by the Java Virtual Machine (JVM):
-
Maven checks the dependencies section in the
pom.xml
file and downloads any missing dependencies. -
Maven looks for
.java
files in thesrc/main/java
directory and ompiles these files into.class
files using the JDK’s Javac compiler. -
Compiled
.class
files are placed in thetarget/classes
directory
# Compile the application
mvn compile
# Shell output:
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.mycompany.app:HelloMaven-app >------------------
[INFO] Building HelloMaven-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ HelloMaven-app ---
[INFO] skip non existing resourceDirectory /home/ubuntu/HelloMaven-app/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ HelloMaven-app ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.188 s
[INFO] Finished at: 2024-11-26T21:29:48Z
[INFO] ------------------------------------------------------------------------
Verify the file and folder structure:
HelloMaven-app
├── pom.xml # Maven configuration file
├── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── mycompany
│ │ └── app
│ │ └── App.java # Main Java file
│ └── test
│ └── java
│ └── com
│ └── mycompany
│ └── app
│ └── AppTest.java # Test file
└── target
├── classes
│ └── com
│ └── mycompany
│ └── app
│ └── App.class # Verify .class file
├── generated-sources
│ └── annotations
├── generated-test-sources
│ └── test-annotations
├── maven-status
│ └── maven-compiler-plugin
│ ├── compile
│ │ └── default-compile
│ │ ├── createdFiles.lst
│ │ └── inputFiles.lst
│ └── testCompile
│ └── default-testCompile
│ ├── createdFiles.lst
│ └── inputFiles.lst
├── surefire-reports
│ ├── com.mycompany.app.AppTest.txt
│ └── TEST-com.mycompany.app.AppTest.xml
└── test-classes
└── com
└── mycompany
└── app
└── AppTest.class
Package the Application (Thin JAR) #
Create a distributable version of the application, such as a JAR (Java Archive) or WAR (Web Application Archive) file, which can be deployed or run:
-
The format is defined by the
<packaging>
tag inpom.xml
. By default, it’sJAR
. -
Ensures the
pom.xml
file is valid and that all dependencies are resolved. -
If not already done, Maven compiles the source files into bytecode and places them in the
target/classes
directory. -
If tests are defined in the
src/test/java
directory, Maven runs them. -
The packaged file, for example
HelloMaven-app-1.0-SNAPSHOT.jar
is placed in thetarget/
directory.
# Package the application
mvn package
# Shell output:
...
[INFO] Building jar: /home/ubuntu/HelloMaven-app/target/HelloMaven-app-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.019 s
[INFO] Finished at: 2024-11-26T21:56:06Z
[INFO] ------------------------------------------------------------------------
Build the Application: With Dependencies #
Compile / Build the Application (Fat JAR) #
# Build a standalone JAR file
mvn clean compile assembly:single
-
clean
Deletes any existing compiled files and build artifacts from the target directory to ensure a fresh build. -
compile
Compiles the Java source code in thesrc/main/java
directory into.class
files and places them in thetarget/classes
directory. -
assembly:single
Invokes themaven-assembly-plugin
to create a fat JAR that includes the compiled application code and all required dependencies bundled together. -
The resulting JAR is placed in the
target
directory:HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar
Run the Packaged Application #
# Run the resulting JAR file
java -jar target/HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar
# Shell output:
Hello, from Maven. Time:
2024-11-27 09:56:15
Containerize the Packaged Application #
Thin Jar #
-
By default, Maven packages only the application’s compiled code in the JAR (thin JAR).
-
Required dependencies like
joda-time
are not bundled inside the JAR and must be provided separately. -
The following example copies the dependencies into the container
Generate the Dependencies #
# Use Maven to copy all runtime dependencies to a directory
mvn dependency:copy-dependencies -DoutputDirectory=target/dependency
Dockerfile #
# Create a Dockerfile
vi Dockerfile
# Use an official OpenJDK runtime as a base image
FROM openjdk:17-jdk-slim
# Set the working directory
WORKDIR /app
# Copy the application's thin JAR to the image
COPY target/HelloMaven-app-1.0-SNAPSHOT.jar app.jar
# Copy all dependencies to the image
COPY target/dependency /app/dependency
# Command to run the application
CMD ["java", "-cp", "app.jar:/app/dependency/*", "com.mycompany.app.App"]
Build Container Image #
# Build the container image
docker build -t hello-maven-app .
Verify the Container Image #
# List images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-maven-app latest ec8a509bb53d 4 seconds ago 410MB
Run the Container #
# Run the container:
docker run --rm hello-maven-app
# Shell output:
Hello, from Maven. Time:
2024-11-27 10:05:35
--rm
Automatically removes the container after it stops.
Fat Jar #
- The Fat Jar includes all it’s necessary dependencies
Dockerfile #
# Create a Dockerfile
vi Dockerfile
# Use a lightweight JRE image as the base image
FROM eclipse-temurin:17-jre-alpine
# Set the working directory
WORKDIR /app
# Copy the application's thin JAR to the image
COPY target/HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar app.jar
# Command to run the application
CMD ["java", "-jar", "app.jar"]
Build Container Image #
# Build the container image
docker build -t hello-maven-app-thin-jar .
Verify the Container Image #
# List images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-maven-app-thin-jar latest acbaa3dc6923 4 seconds ago 186MB
hello-maven-app latest ec8a509bb53d 5 minutes ago 410MB
Run the Container #
# Run the container:
docker run --rm hello-maven-app-thin-jar
# Shell output:
Hello, from Maven. Time:
2024-11-27 10:10:41
--rm
Automatically removes the container after it stops.
Multistage Dockerfile #
Dockerfile #
-
Build Stage: The application is built in a dedicated environment with all necessary tools and dependencies.
-
Runtime Stage: A lightweight base image
eclipse-temurin:17-jre-alpine
is used to copy only the compiled Fat Jar from the build stage.
# Create a Dockerfile
vi Dockerfile
### Stage 1: Build Stage
# Use a lightweight JRE image as the base image / define as build stage
FROM maven:3.9.4-eclipse-temurin-17 AS build
# Set the working directory inside the build stage
WORKDIR /app
# Copy the Maven project files into the container
COPY pom.xml ./
COPY src ./src
# Build the application with dependencies included
RUN mvn clean compile assembly:single
### Stage 2: Runtime Stage
# Use a lightweight JRE image as the base image
FROM eclipse-temurin:17-jre-alpine
# Set the working directory inside the runtime stage
WORKDIR /app
# Copy the built JAR file from the build stage to the runtime stage
COPY --from=build /app/target/HelloMaven-app-1.0-SNAPSHOT-jar-with-dependencies.jar app.jar
# Expose port 80 for the web server
EXPOSE 80
# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
Build Container Image #
# Build the container image
docker build -t maven-webserver-multistage .
Verify the Container Image #
# List images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
maven-webserver-multistage latest f9e9fe1d8aa2 12 seconds ago 189MB
Run the Container #
# Run the container:
docker run --rm maven-webserver-multistage
# Shell output:
Hello, from Maven. Time:
2024-11-27 13:47:55