Jakarta EE and Docker: What You Should Know
As a Java developer, you might wonder why you should be concerned about Docker. This will show you why you should and how to use Jakarta EE.
Join the DZone community and get the full member experience.
Join For FreeAt the first contact at the Jakarta EE website, a user can see the slogan: "Open Source Cloud-Native Java," but what does cloud-native mean? There's still a discussion about it, but a right approach is cloud-native is a term to describe container-based environments. A fantastic tool to help with container applications is Docker that provides PODA (Package Once Deploy Anywhere) that allows running in any cloud provider, this concept is what Java has been doing with WORA (Write Once Run Anywhere) in the last twenty years. The goal of this article is to cover some tips and good practices around Java and Jakarta EE in cloud-native age with Docker.
Why Should I Care About Docker?
In the world of several technologies, as a Java developer, why should you care about Docker right now? You are late! Docker has become a sophisticated tool to use cloud computing, so here are some reasons to care:
- Docker does not allow vendor-lock-in that means; with Docker a developer can use any cloud provider that supports Docker.
- It helps you reduce the impedance mismatch between dev, test, and production environment.
- It simplifies Java application deployment.
- Reduces process to install services such as a database.
What is a Container?
From the Docker documentation:
A container is a standard unit of software that packages up the code and all its dependencies, so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries, and settings.
Follow the Leaders
When we start to learn new technology, following people who know about this technology is a good practice, below my four recommendations about Java and Docker:
Installation
It is out of the scope to talk about the installation process to both Docker and docker-compose; otherwise, this post would become too big. But, there is an excellent tutorial to teach how to install:
Hello World From the Docker side
As the first sample code, this post will use a Java 8 Maven project that a developer can create from any Maven archetype such as the one below:
mvn archetype:generate -DgroupId=org.soujava.docker -DartifactId=hello-world-docker -DinteractiveMode=false
With the project created, the next step is to change the "Hello World" message and at the pom.xml define the class to run in a jar file.
package org.soujava;
public class App
{
public static void main( String[] args )
{
System.out.println("Hello World from Docker!");
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.soujava.docker</groupId>
<artifactId>hello-world-docker</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>hello-world-docker</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compile.version>3.8.0</maven.compile.version>
<maven.surefire.plugin.version>2.22.1</maven.surefire.plugin.version>
<docker.plugin.version>1.2.0</docker.plugin.version>
<maven.jar.plugin.version>3.1.1</maven.jar.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compile.version}</version>
<configuration>
<target>${maven.compiler.target}</target>
<source>${maven.compiler.source}</source>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.plugin.version}</version>
<configuration>
<archive>
<manifest>
<mainClass>org.soujava.docker.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Before getting into Docker, it will package and execute as a regular Java application.
mvn clean package
java -jar target/hello-world-docker-1.0-SNAPSHOT.jar
Hello World from Docker! #output
With the jar package ready, the next step is to create a Dockerfile in the root of this project, with the first Java Docker image application. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
FROM openjdk:8
COPY target/hello-world-docker-1.0-SNAPSHOT.jar /usr/src/hello-world-docker.jar
CMD java -jar /usr/src/hello-world-docker.jar
With the Dockerfile ready, the next step is to create an image from that script.
docker image build -t hello-world-docker-java:latest .
Finally, the first "Hello World" image with Java; the next step is to run it.
docker container run hello-world-docker-java:latest
Hello World from Docker!#output
To create a Java Docker image, it requires you to create a jar file then create a new image from the OpenJDK tag eight, then appends the jar file in the container, to in brief execute it. To make this process smoother, a developer might create a shell script to hide the process pipe into one file to run it as a "build.sh."
#!/usr/bin/env bash
mvn clean package #java package
docker image build -t hello-world-docker-java:latest . #build the image
docker container run hello-world-docker-java:latest #execute as container
The shell script is better than executing each step manually, however, that's still far from the right way to do it.
Maven Docker Plugin
A smooth integration between Java Maven Application and Docker is possible because there are several plugins. The post covers the Spotify Docker plugin that has several features to create, build, and push an image to a Docker repository. Set the plugin and append the docker-maven-plugin within the plugins tag that is under the build tag so the pom.xml update looks like the code below.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.soujava.docker</groupId>
<artifactId>hello-world-docker</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>hello-world-docker</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compile.version>3.8.0</maven.compile.version>
<maven.surefire.plugin.version>2.22.1</maven.surefire.plugin.version>
<docker.plugin.version>1.2.0</docker.plugin.version>
<maven.jar.plugin.version>3.1.1</maven.jar.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compile.version}</version>
<configuration>
<target>${maven.compiler.target}</target>
<source>${maven.compiler.source}</source>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.plugin.version}</version>
<configuration>
<archive>
<manifest>
<mainClass>org.soujava.docker.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.plugin.version}</version>
<configuration>
<imageName>hello-world-docker-java-plugin</imageName>
<baseImage>openjdk:8</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
<forceTags>true</forceTags>
<imageTags>
<imageTag>${project.version}</imageTag>
<imageTag>latest</imageTag>
</imageTags>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>
The plugin makes an image named "hello-world-docker-java-plugin" from openjdk:8
, which executes the jar file in Docker. furthermore, it defines the tag to the image that is the lastest and the Maven project version.
mvn clean package docker:build
To execute the container, let's use the same command that we used previously.
docker container run hello-world-docker-java-plugin
Hello World from Docker!#output
Jakarta EE sample With Docker
On the Jakarta EE sample, this post uses an application resource. Using JAX-RS, a GET request will return the IP address. As a dependency, it requires Jakarta EE 8 at the pom.xml.
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>2.1.5</version>
<scope>provided</scope>
</dependency>
In the InetAddress class, it returns both the host address and hostname, the scope of this sample is given an HTTP GET request it will return the IP address.
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("resource")
public class ApplicationResource extends Application {
}
import java.net.InetAddress;
public class HelloWorld {
private final String address;
private final String name;
public HelloWorld(InetAddress inetAddress ) {
this.address = inetAddress.getHostAddress();
this.name = inetAddress.getHostName();
}
public String getAddress() {
return address;
}
public String getName() {
return name;
}
}
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.net.InetAddress;
import java.net.UnknownHostException;
@Path("hello")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
@GET
public HelloWorld hello() throws UnknownHostException {
return new HelloWorld(InetAddress.getLocalHost());
}
}
The beauty of Jakarta EE is that it is not locked in, so a Java developer can choose several server vendors. On this sample, it uses the newest Apache TomEE implementation. When the server is up, with an HTTP request, the result will be an IP address and hostname.
curl http://localhost:8080/helloworld/resource/hello
{"address":"127.0.1.1","name":"otaiojava"} #output
Jakarta EE With WAR file
A Jakarta EE sample with a war file has the benefit of being easier to change the server implementation. Once the package makes a war file, it needs to take a Jakarta EE server Docker image, then append its file. Therefore, using the Spotify docker-maven-plugin after the package will put the generated file on the Apache TomEE image.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.soujava.docker</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>hello-world-docker-war</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>2.1.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>hello-world</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.plugin.version}</version>
<configuration>
<imageName>hello-world-java-war</imageName>
<baseImage>tomee:latest</baseImage>
<forceTags>true</forceTags>
<imageTags>
<imageTag>${project.version}</imageTag>
<imageTag>latest</imageTag>
</imageTags>
<resources>
<resource>
<targetPath>/usr/local/tomee/webapps/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.war</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>
With the pom.xml set, just run a Maven command.
mvn clean package docker:build
With the image ready, it starts the application.
docker container run -p 8080:8080 -e JAVA_OPTS='-Xmx100m' hello-world-java-war
With the image running and the request made, the result will be different that is because the IP will use the Docker network.
curl http://localhost:8080/hello-world/resource/hello
{"address":"172.17.0.2","name":"a487b564182a"}//output
Jakarta EE with uber-JAR
An uber-JAR, also known as a fat JAR or JAR with dependencies, is a JAR file that contains not only a Java program but embeds its dependencies as well. Each Jakarta EE server provider has its plugin to execute and generate its uber jar.
<plugin>
<groupId>org.apache.tomee.maven</groupId>
<artifactId>tomee-maven-plugin</artifactId>
<version>${tomee.version}</version>
</plugin>
With the tomee-maven-plugin under the build-> plugins tag, the project is ready to create a fat-jar. Consequently, it can execute mvn tomee:exec
, mvn tomee:run
and so on. To run the Apache TomEE, run the mvn tomee:run
command.
curl http://localhost:8080/hello-world/resource/hello
{"address":"127.0.1.1","name":"otaiojava"} #output
To execute as uber-jar, it will follow the same principle of the first "Hello World." Accordingly, it will append a jar in an OpenJDK image and run it.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.soujava.docker</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>hello-world-docker-uber-jar</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>2.1.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>hello-world</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.plugin.version}</version>
<configuration>
<imageName>hello-world-uber-jar</imageName>
<baseImage>openjdk:8</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}-exec.jar"]</entryPoint>
<forceTags>true</forceTags>
<imageTags>
<imageTag>${project.version}</imageTag>
<imageTag>latest</imageTag>
</imageTags>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}-exec.jar</include>
</resource>
</resources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomee.maven</groupId>
<artifactId>tomee-maven-plugin</artifactId>
<version>${tomee.version}</version>
</plugin>
</plugins>
</build>
</project>
Create a Maven command to make build a Docker image.
mvn clean install tomee:exec docker:build
The uber-jar is image-ready. Execute:
docker container run -p 8080:8080 -e JAVA_OPTS='-Xmx100m' hello-world-uber-jar
Uber-jar vs. War in Docker
There is a difference in Docker performance when we talk about uber-jar vs. a Docker image with a container. As Ondro Mihályi says: "Many containers share docker image. Same with app servers. Fat JARs duplicate lots of stuff." There is also an Ivar presentation that talks about it.
Docker-Compose
Compose is a tool for defining and running multi-container Docker applications. With docker-compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration. To shows integrations between Jakarta EE server and multiple docker images, and it will create three Apache TomEE on Docker with a load balancer on the top.
version: '3.2'
services:
server-1:
image: "hello-world-java-war"
networks:
- docker-java
ports:
- 8080:8080
environment:
- JAVA_OPTS='-Xmx100m'
server-2:
image: "hello-world-java-war"
networks:
- docker-java
ports:
- 8081:8080
environment:
- JAVA_OPTS='-Xmx100m'
server-3:
image: "hello-world-java-war"
networks:
- docker-java
ports:
- 8082:8080
environment:
- JAVA_OPTS='-Xmx100m'
loadbalancer:
image: "jasonwyatt/nginx-loadbalancer"
networks:
- docker-java
ports:
- 80:80
depends_on:
- server-1
- server-2
- server-3
env_file:
- env.list
networks:
docker-java:
You can pass multiple environment variables from an external file through to a service’s containers with the env_file
option, just like with docker run --env-file=FILE
...:
# automatically created environment variables (docker links)
TOMEE_1_PORT_8080_TCP_ADDR=server-1
TOMEE_2_PORT_8080_TCP_ADDR=server-2
TOMEE_3_PORT_8080_TCP_ADDR=server-3
# special environment variables
TOMEE_PATH=/hello-world
TOMEE_REMOTE_PORT=8080
TOMEE_REMOTE_PATH=/hello-world
TOMEE_HOSTNAME=loadbalance
With both the docker-compose and env.list, it can start several containers with just one command.
docker-compose up -d
With multiple requests, the result will be different because each request returns from a separate server.
curl -get http://localhost/hello-world/resource/hello {"address":"172.20.0.4","name":"c4f4072fe5eb"}
curl -get http://localhost/hello-world/resource/hello {"address":"172.20.0.2","name":"f043303ab2c5"}
curl -get http://localhost/hello-world/resource/hello {"address":"172.20.0.3","name":"b25e6d2e3227"}
curl -get http://localhost/hello-world/resource/hello {"address":"172.20.0.4","name":"c4f4072fe5eb"}
curl -get http://localhost/hello-world/resource/hello {"address":"172.20.0.2","name":"f043303ab2c5"}
Test with Containers
To test using Docker in Java, there is a library, the Testcontainers, that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
With the dependency ready the next step is to create and test a container. This framework has excellent features, such as the ability to run docker-compose.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
public class AppTest {
@Test
public void shouldAnswerWithTrue() {
final GenericContainer mongodb =
new GenericContainer("mongo:latest")
.withExposedPorts(27017)
.waitingFor(Wait.defaultWaitStrategy());
mongodb.start();
Assertions.assertNotNull(mongodb.getContainerIpAddress());
Assertions.assertNotNull(mongodb.getFirstMappedPort());
mongodb.stop();
}
}
With the Testcontainer a Java developer can create several containers to create an integration test. The integration test is very important, however, the unit test is, as well. If there is a huge volume of an integration test that means there is a loose couple in the following architecture layer such as MVC. To learn more about good practices in code desing there is this video from Venkat Subramaniam.
This post covers the integration between Docker and the new generation of Java EE, Jakarta EE, that has a focus on cloud-native. Both Docker and Jakarta EE have the approach of being ready to go to production anywhere. As a Java community member, we expected much more integration to rocket the platform to the cloud.
References
Opinions expressed by DZone contributors are their own.
Comments