Skip to content

Commit 395f446

Browse files
author
wlanboy
committed
added JAVA_OPTS Support and Docker with JLINK
1 parent 02492dc commit 395f446

File tree

5 files changed

+258
-12
lines changed

5 files changed

+258
-12
lines changed

Dockerfile25

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# ============================
2+
# 1. Build Stage (Java 25)
3+
# ============================
4+
FROM registry.access.redhat.com/ubi9/openjdk-25:latest AS build
5+
# Eclipse Temurin bietet aktuelle Java-Versionen inkl. Java 25
6+
7+
WORKDIR /app
8+
9+
COPY pom.xml .
10+
# → Nur die pom.xml wird kopiert, damit Maven bereits alle Dependencies auflösen kann,
11+
# ohne dass sich der Sourcecode ändert. Das verbessert das Layer-Caching.
12+
13+
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests dependency:go-offline
14+
# → Lädt alle Maven-Dependencies vorab herunter.
15+
# → --mount=type=cache sorgt dafür, dass das lokale Maven-Repository zwischen Builds gecached wird.
16+
17+
COPY src ./src
18+
# → Jetzt erst der Sourcecode, damit Änderungen am Code nicht das Dependency-Layer invalidieren.
19+
20+
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests compile spring-boot:process-aot package
21+
# → Baut das eigentliche JAR mit AOT (Ahead-of-Time) Processing.
22+
# → compile: Kompiliert die Klassen (notwendig für process-aot).
23+
# → spring-boot:process-aot: Generiert AOT-Metadaten basierend auf den kompilierten Klassen.
24+
# → package: Baut das finale JAR inkl. AOT-Klassen.
25+
# → Wieder mit Maven-Cache, um Build-Zeit zu sparen.
26+
27+
RUN cp target/javahttpclient-0.0.1-SNAPSHOT.jar app.jar && \
28+
java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
29+
# → Spring Boot 4.x Layertools: --launcher ist erforderlich um den Loader zu extrahieren
30+
# → Extrahierte Layer:
31+
# - dependencies (BOOT-INF/lib)
32+
# - spring-boot-loader (org/springframework/boot/loader/*)
33+
# - snapshot-dependencies
34+
# - application (BOOT-INF/classes)
35+
# → Vorteil: Docker kann diese Layer getrennt cachen → schnellere Deployments.
36+
37+
# ============================
38+
# 2. Runtime Stage (Java 25)
39+
# ============================
40+
FROM registry.access.redhat.com/ubi9/openjdk-25-runtime:latest
41+
42+
# OCI-konforme Labels
43+
LABEL org.opencontainers.image.title="Java http client" \
44+
org.opencontainers.image.description="Spring based rest service to test http routes in kubernetes." \
45+
org.opencontainers.image.version="0.0.1-SNAPSHOT" \
46+
org.opencontainers.image.vendor="wlanboy" \
47+
org.opencontainers.image.source="https://github.com/wlanboy/JavaHttpClient" \
48+
org.opencontainers.image.licenses="MIT" \
49+
org.opencontainers.image.base.name="ubi9/openjdk-25-runtime"
50+
51+
WORKDIR /app
52+
53+
USER root
54+
# → Temporär root, um Verzeichnisse anzulegen und Berechtigungen zu setzen.
55+
56+
RUN mkdir -p /app/config /app/data && \
57+
chown -R 185:0 /app && \
58+
chmod -R g+w /app
59+
# → /app/config: für externe Konfigurationen
60+
# → /app/data: für persistente Daten
61+
# → Non-root User für sicheren Betrieb
62+
63+
USER 185
64+
# → Zurück zum nicht-privilegierten User.
65+
66+
COPY --from=build --chown=185:185 /app/extracted/dependencies/ ./
67+
# → Kopiert nur die Dependency-Layer. Ändern sich selten.
68+
69+
COPY --from=build --chown=185:185 /app/extracted/spring-boot-loader/ ./
70+
# → Enthält den Spring Boot Launcher (Main-Class Loader). Ändern sich selten.
71+
72+
COPY --from=build --chown=185:185 /app/extracted/snapshot-dependencies/ ./
73+
# → Snapshot-Dependencies (z. B. lokale libs), ändern sich häufiger.
74+
75+
COPY --from=build --chown=185:185 /app/extracted/application/ ./
76+
# → Der eigentliche Applikationscode (Kompilat). Ändert sich.
77+
78+
COPY --chown=185:185 containerconfig/application.properties /app/config/application.properties
79+
# → Externe Konfiguration ins Config-Verzeichnis für die Referenz für ENV Vars
80+
81+
COPY --chown=185:185 entrypoint.sh /app/entrypoint.sh
82+
# → Custom Entrypoint für Java OPTS.
83+
84+
EXPOSE 8080
85+
# → Dokumentiert den Port, den die App verwendet (Spring Boot Default).
86+
87+
HEALTHCHECK --interval=30s --timeout=3s \
88+
CMD curl -f http://localhost:8080/actuator/health || exit 1
89+
# → Nutzt den Spring Boot Actuator Health Endpoint.
90+
# → Alternativ: wget oder ein einfacher TCP-Check
91+
92+
ENTRYPOINT ["/app/entrypoint.sh"]
93+
# → Startet die App über das Entry-Skript.
94+
# → Vorteil: Skript kann Umgebungsvariablen verarbeiten, ENTRYPOINT nicht.

Dockerfile25Jlink

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# ============================
2+
# 1. Build Stage (Java 25)
3+
# ============================
4+
FROM registry.access.redhat.com/ubi9/openjdk-25:latest AS build
5+
WORKDIR /app
6+
7+
# Maven Caching & Build (wie gehabt)
8+
COPY pom.xml .
9+
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests dependency:go-offline
10+
COPY src ./src
11+
RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests compile spring-boot:process-aot package
12+
13+
# JAR extrahieren für Layering
14+
RUN cp target/javahttpclient-0.0.1-SNAPSHOT.jar app.jar && \
15+
java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
16+
17+
# ============================
18+
# 2. JRE Creation Stage (NEU)
19+
# ============================
20+
21+
# WICHTIG: Umschalten auf root, um Pakete zu installieren
22+
USER root
23+
# Installiere die JMODS, die für jlink zwingend erforderlich sind. Red Hat trennt das, um Platz zu sparen.
24+
# Wenn jlink anweist, Debug-Symbole zu entfernen (--strip-debug), sucht das Tool im Hintergrund
25+
# nach dem System-Befehl objcopy (ein Teil der binutils), was natürlich nicht als notwendig erachtet wurde.
26+
RUN microdnf install -y java-25-openjdk-jmods binutils && \
27+
microdnf clean all
28+
29+
# Wir analysieren die extrahierten Layer, um ein minimales JRE zu bauen
30+
RUN jdeps \
31+
--ignore-missing-deps \
32+
--print-module-deps \
33+
--multi-release 25 \
34+
--recursive \
35+
--class-path "extracted/dependencies/*:extracted/snapshot-dependencies/*" \
36+
extracted/application/BOOT-INF/classes > modules.txt
37+
38+
# Erzeuge das Custom JRE
39+
# --compress zip-9 ist der moderne Ersatz für --compress=2
40+
# NoClassDefFoundError: java.beans.PropertyEditorSupport -> java.desktop
41+
# jmx, metrics -> java.management
42+
# load jar files (swagger) -> jdk.zipfs
43+
# needs jlink -> java.instrument
44+
# jpa needs java.sql.Date -> java.sql
45+
# jetty jndi config -> java.naming
46+
# jdk25 warnings -> sun.misc.Unsafe, das von Objenesis/CGLIB -> jdk.unsupported
47+
RUN $JAVA_HOME/bin/jlink \
48+
--add-modules $(cat modules.txt),jdk.crypto.ec,jdk.charsets,java.desktop,java.management,jdk.zipfs,java.instrument,java.sql,java.naming,jdk.unsupported \
49+
--strip-debug \
50+
--no-man-pages \
51+
--no-header-files \
52+
--compress zip-9 \
53+
--output /custom-jre
54+
55+
# Erzeuge die Klassenliste für AppCDS
56+
RUN /custom-jre/bin/java -Djarmode=tools -jar app.jar extract --layers --destination cds-extraction
57+
58+
# Erzeugt das Basis-Archiv für das Custom JRE
59+
RUN /custom-jre/bin/java -Xshare:dump
60+
61+
# CDS Archiv generieren
62+
# 1. Liste der Klassen erstellen (Training)
63+
# WICHTIG: Wir nutzen /custom-jre/bin/java, damit das Archiv zum JRE passt!
64+
# spring.context.exit weist Spring Boot an, den Application Context aufzubauen (damit alle Klassen geladen werden),
65+
# aber die App sofort danach sauber zu beenden
66+
# Training gegen die entpackten Layer (muss identisch zum Start sein!)
67+
RUN /custom-jre/bin/java -XX:ArchiveClassesAtExit=app.jsa \
68+
-Dspring.context.exit=onRefresh \
69+
-Dspring.aot.enabled=true \
70+
-cp "extracted/dependencies/*:extracted/snapshot-dependencies/*:extracted/application/" \
71+
org.springframework.boot.loader.launch.JarLauncher || [ -f app.jsa ]
72+
# Hinweis: Das "|| true" ist wichtig, da die App beim Training mangels DB/Config evtl. abbricht,
73+
# aber das Archiv trotzdem schreibt.
74+
75+
# ============================
76+
# 3. Runtime Stage (Minimales OS)
77+
# ============================
78+
# Wir nutzen hier ubi9-minimal statt der vollen Java-Runtime
79+
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
80+
81+
LABEL org.opencontainers.image.title="Java http client with Custom JRE" \
82+
org.opencontainers.image.version="0.0.1-SNAPSHOT"
83+
84+
WORKDIR /app
85+
86+
# Kopiere das Custom JRE aus der Build-Stage
87+
COPY --from=build /custom-jre /opt/jre
88+
ENV PATH="/opt/jre/bin:$PATH"
89+
90+
# User-Setup (UBI-Minimal braucht oft manuelles ID-Handling)
91+
# curl-minial for healthcheck
92+
RUN microdnf install -y shadow-utils curl-minimal && \
93+
groupadd -r appgroup -g 185 && \
94+
useradd -r -u 185 -g appgroup -d /app -s /sbin/nologin appuser && \
95+
mkdir -p /app/config /app/data && \
96+
chown -R 185:185 /app && \
97+
microdnf clean all
98+
99+
USER 185
100+
101+
# Layer kopieren (wie gehabt)
102+
COPY --from=build --chown=185:185 /app/extracted/dependencies/ ./
103+
COPY --from=build --chown=185:185 /app/extracted/spring-boot-loader/ ./
104+
COPY --from=build --chown=185:185 /app/extracted/snapshot-dependencies/ ./
105+
COPY --from=build --chown=185:185 /app/extracted/application/ ./
106+
COPY --from=build --chown=185:185 /app/app.jsa /app/app.jsa
107+
COPY --chown=185:185 containerconfig/application.properties /app/config/application.properties
108+
COPY --chown=185:185 entrypoint.sh /app/entrypoint.sh
109+
110+
EXPOSE 8080
111+
HEALTHCHECK --interval=30s --timeout=3s \
112+
CMD curl -f http://localhost:8080/actuator/health || exit 1
113+
114+
ENTRYPOINT ["/app/entrypoint.sh"]

README.md

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,63 @@
11
# JavaHttpClient
2+
23
Spring based rest service to test http routes in kubernetes.
34
Showing http status, response, timing and istio envy settings.
45

56
## Web ui
7+
68
![web ui](./screenshots/httpclient-webui.png)
79

810
## Istio tab
11+
912
![istio tab](./screenshots/httpclient-istiotab.png)
1013

1114
## Swagger
15+
1216
![istio tab](./screenshots/httpclient-swagger.png)
1317

14-
# build
15-
* mvn package
18+
# Build
19+
20+
```bash
21+
mvn package
22+
```
23+
24+
# Docker build
25+
26+
```bash
27+
docker build -t wlanboy/javahttpclient:latest .
28+
```
29+
30+
# Docker build with jlink and without
31+
32+
```bash
33+
docker build -f Dockerfile25Jlink -t wlanboy/javahttpclient:jlink .
34+
docker build -f Dockerfile25 -t wlanboy/javahttpclient:jre .
35+
36+
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | grep "javahttpclient"
37+
wlanboy/javahttpclient jre 510MB
38+
wlanboy/javahttpclient jlink 223MB
39+
```
1640

17-
# docker build
18-
* docker build -t wlanboy/javahttpclient:latest .
41+
# Run container
1942

20-
# run container
21-
* docker run --rm --name httpclient --publish 8080:8080 wlanboy/javahttpclient:latest
43+
```bash
44+
docker run --rm --name httpclient --publish 8080:8080 wlanboy/javahttpclient:latest
2245

23-
# docker hub
46+
docker run --rm --name httpclient --publish 8080:8080 wlanboy/javahttpclient:jlink
47+
```
48+
49+
# Docker hub
2450
* https://hub.docker.com/r/wlanboy/javahttpclient
2551

26-
# test java http client
52+
# Test java http client
53+
2754
```bash
2855
curl -L -X POST 'http://127.0.0.1:8080/client' -H 'Content-Type: application/json' \
2956
-d '{"url" : "https://github.com", "copyHeaders": "false"}'
3057
```
58+
3159
# local dev
60+
3261
```bash
3362
curl -fsSL https://raw.githubusercontent.com/metalbear-co/mirrord/main/scripts/install.sh | bash
3463

@@ -39,14 +68,16 @@ mirrord exec -n javahttpclient --target deployment/javahttpclient -- mvn spring-
3968
mirrord exec -n javahttpclient --target pod/$POD -- java -jar target/javahttpclient-0.0.1-SNAPSHOT.jar
4069

4170
mirrord exec -n javahttpclient --target deployment/javahttpclient -- java -jar target/javahttpclient-0.0.1-SNAPSHOT.jar
42-
4371
```
4472

4573
# swagger uri
46-
- http://localhost:8080/swagger-ui/index.html#/http-client-controller/postMapping
74+
75+
* http://localhost:8080/swagger-ui/index.html#/http-client-controller/postMapping
4776

4877
# curl calls for mirrorservice
78+
4979
* see: https://github.com/wlanboy/MirrorService
80+
5081
```bash
5182
curl -X 'POST' \
5283
'http://localhost:8080/client' \
@@ -69,4 +100,5 @@ curl -X 'POST' \
69100
"body": "",
70101
"copyHeaders": true
71102
}'
72-
```
103+
```
104+

entrypoint.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
set -e
66
# Beendet das Skript sofort, wenn ein Befehl einen Fehler zurückgibt.
77

8+
# Erlaubt das Hinzufügen von Optionen über eine Umgebungsvariable,
9+
# ohne den Entrypoint im Image ändern zu müssen.
10+
: "${JAVA_OPTS:=}"
11+
812
# Wir nutzen exec, damit Java die PID 1 übernimmt.
913
# Dies ist wichtig für das Signal-Handling (z.B. in Kubernetes).
1014
# exec ersetzt den aktuellen Shell-Prozess durch den Java-Prozess.
@@ -19,6 +23,8 @@ set -e
1923
# -XX:+ExitOnOutOfMemoryError: JVM beendet bei OOM (Kubernetes kann neustarten)
2024

2125
exec java \
26+
$JAVA_OPTS \
27+
-XX:SharedArchiveFile=/app/app.jsa \
2228
-Djava.security.egd=file:/dev/./urandom \
2329
-Dspring.aot.enabled=true \
2430
-XX:MaxRAMPercentage=50 \

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<parent>
88
<groupId>org.springframework.boot</groupId>
99
<artifactId>spring-boot-starter-parent</artifactId>
10-
<version>4.0.1</version>
10+
<version>4.0.3</version>
1111
<relativePath /> <!-- lookup parent from repository -->
1212
</parent>
1313

0 commit comments

Comments
 (0)