Skip to main content

Dockerise a Spring webservice with Java 17 and Jlink


1. Where is JRE ?


If you haven't worked with Java for a long time and have just returned, you may be surprised to find that no JRE packages are included with Java 17. The reason for that is because Oracle no longer expects the general end user to have a Java runtime installed on their system, hence only JDK is provided (see here). 


2. Does it means that I have to use JDK to dockerise my Spring application ?


Well, you obviously can use JDK to dockerise your application. However, as JDK is generally large in size and consumes quite a lot of resources, it is not really a good idea to embed JDK inside your application to run in a microservices environment.

There are two better choices to dockerise your Spring applications:
  1. Use a pre-built JRE from other providers, e.g., https://hub.docker.com/_/eclipse-temurin
  2. Build your own JRE with only the components that you need with jlink and jdeps
For the first option, it can be seen that there are some pre-built JRE versions out there that you can directly embed into your Docker application. It is clearly better than using JDK pre-built version. But in our opinion, the best option is to automatically build a JRE version that contains only what your application needs. 


3. What are jlink and jdeps ?


Since version 9, Java has been moving towards a module system. With this module system, all the Java Platform APIs have been split up into separate modules, allowing us to specify which modules of the Java platform that our application requires. Jdeps and jlink are the tools that allow us to automatically determine these required modules and assemble them together.

Particularly:

Jdeps is a program that is able to scan and analyse your Java application and show which modules you need in order to run your application.

Jlink is another program that takes the list of the required modules to assemble and optimise into a custom JRE.


4. How can I use jlink and jdeps to dockerise my Spring application ?


First thing you need to do is to collect and put your application's dependencies into a particular folder. Suppose that you have created a spring gradle project, then add the following lines into your build.gradle configuration file:
task deps(type: Copy) {
	from configurations.runtimeClasspath
	into "$buildDir/lib"
}
Here, we tell gradle to copy all the dependencies to the lib folder of the current build directory. Next, we need to define jar task to build our jar application, which uses the dependencies in the lib folder that we have just created:
jar {
  manifest {
    attributes["Main-Class"] = "com.mtvu.websocketserver.WebsocketServerApplication"
    attributes["Class-Path"] = configurations.runtimeClasspath.collect { 'lib/' + it.getName() }.join(' ')
  }
}
You would want to replace the Main-Class attribute to the path of your main class, accordingly. 

Now, for the Dockerfile part, in order to have a Java 17 JRE customised according to your application, we need to use multi-stage builds with each stages are described as below:

Stage 1: build dependencies and jar

FROM openjdk:18-alpine AS deps-build
WORKDIR /app

# copy the dependencies into the docker image
COPY gradle gradle
COPY gradlew .
COPY build.gradle .
COPY settings.gradle .
COPY src src

RUN sh gradlew clean deps build

# copy the executable jar into the docker image
RUN cp build/libs/*-SNAPSHOT.jar build/app.jar


# find JDK dependencies dynamically from jar
RUN jdeps \
--ignore-missing-deps \
# suppress any warnings printed to console
-q \
# java release version targeting
--multi-release 17 \
# output the dependencies at end of run
--print-module-deps \
# specify the the dependencies for the jar
--class-path="./build/lib/*" \
--module-path="./build/lib/*" \
# pipe the result of running jdeps on the app jar to file
build/app.jar > jre-deps.info
Here we use OpenJDK 18 instead of 17 to build the application. The reason for that is because there are some errors with jdeps in OpenJDK 17, which doesn't allow us to build list of dependencies for our application. However, it is not really a problem because we just use OpenJDK 18 to obtain the list of dependencies. In the next stage, we will use the generated jre-deps.info to build JRE using OpenJDK 17.

Stage 2: build JRE

FROM openjdk:17-alpine AS jre-build
WORKDIR /app

COPY --from=deps-build /app/jre-deps.info jre-deps.info

RUN jlink --verbose \
--compress 2 \
--strip-java-debug-attributes \
--no-header-files \
--no-man-pages \
--output jre \
--add-modules $(cat jre-deps.info)
Here, we take the jre-deps.info file from the first build stage and use jlink with OpenJDK 17 to build JRE for our application. The compiled JRE will be coppied to the last build stage to finalise our docker application.

Stage 3: put everything together

FROM alpine:3.16.2
WORKDIR /deployment

# copy the custom JRE produced from jlink
COPY --from=jre-build /app/jre jre

# copy the app dependencies
COPY --from=deps-build /app/build/lib/* lib/

# copy the app
COPY --from=deps-build /app/build/app.jar app.jar

# run the app on startup
ENTRYPOINT jre/bin/java -jar app.jar
It can be seen that we have gathered all pieces of our application, including the application app.jar, the dependencies in lib folder, and JRE runtime image together in an alpine docker container. Now, to build your docker container, simply run:

docker build . 

And that's it. 
A simple implementation of this tutorial can be found here: https://github.com/dispatcher-servlet/dockerise-spring-java17

Comments

Popular posts from this blog

Spring WebSocket authentication with JWT Spring Security

1. Overview In this tutorial, we will talk about how to authenticate your Spring websocket server using JWT Spring Secutiry. Suppose that you already have a stateless microservice with an authorisation server up and running. Now you would want that all of your websocket service instances have to connect to the authorisation service to validate user's access token before allowing them to establish a websocket connection to your server. The official Spring document [1] has mentioned about this. However, they don't clarify how can we obtain the Authentication object. One can write a custom token validation mechanism and validate the user header token manually. But in our opinion, it is best to avoid doing this, as it may introduce some security vulnerabilities. In this tutorial, we will walk you though step-by-step of how to validate user's access token by reusing the JwtAuthentication provided by Spring Security framework. 2. Gradle Dependencies Since this is a Gradle-ba...

Spring Websocket integration test with JWT Authentication

1. Overview In our  previous tutorial  we have introduced an approach to authenticate a user when connecting to our websocket server using JWT authentication.  In this article, we will see how can we perform integration tests to check the behaviours of our security layers in different scenarios, as well as ensuring the correctness of our implementation for future updates. 2. Gradle dependencies Since this is a Gradle-based project, we add the required testing dependencies to the build.gradle: testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0' testImplementation 'org.awaitility:awaitility-kotlin:4.2.0' testImplementation 'io.projectreactor:reactor-test' 3. Defining a Controller endpoint for testing In this example, we will write some integration tests to validate the requests to access a controller endpoint, as defined below: @Controller public class WebsocketCon...