Apache Juneau Docker Compose 部署指南
以 Maven+JDK 镜像构建与运行 Apache Juneau 9.2.0 演示工程,用 Compose 编排 juneau-marshall 构建服务与 juneau-microservice-jetty REST 常驻服务,真实启动、curl 验证与运维命令全流程。
Apache Juneau Docker Compose 部署指南
目标:Apache Juneau 9.2.0 以 Maven 依赖形态提供。本文以「教程」形态,用官方
maven:3.9-eclipse-temurin-17镜像编排两个服务——juneau-demo-build(juneau-marshall 序列化示例,一次性构建)与juneau-rest(juneau-microservice-jetty REST 微服务,常驻运行),完成真实启动、curl 验证、日志巡检与停止卸载全流程。已在 Docker 29.8.1 + Compose v5.5.1 环境实测通过。
一、环境信息
- 宿主系统:Linux x86_64
- Docker 版本:Docker version 29.8.1
- Compose 插件:Docker Compose version v5.5.1
- 基础镜像:
maven:3.9-eclipse-temurin-17(Maven 3.9 + Eclipse Temurin JDK 17) - Apache Juneau:9.2.0(juneau-microservice-jetty + juneau-rest-client + juneau-marshall)
- 部署目录:
/opt/data/quzuo/juneau-compose(含 juneau-demo、juneau-rest 两个工程子目录) - REST 服务端口:10000(映射为宿主
10000:10000) - 常驻服务 JVM 堆:-Xms256m / -Xmx512m
二、前置条件
- 宿主机已安装 Docker 与 Compose 插件,并验证版本:
docker --version
真实回显:
Docker version 29.8.1, build 2c2d5e6
docker compose version
真实回显:
Docker Compose version v5.5.1
- 宿主机可联网拉取
maven:3.9-eclipse-temurin-17镜像与 Maven Central 依赖。
三、版本选型说明
| 组件 | 版本 | 说明 |
|---|---|---|
| maven 镜像 | maven:3.9-eclipse-temurin-17 | 自带 Maven 3.9 与 JDK 17,构建/运行合一 |
| juneau-microservice-jetty | 9.2.0 | REST 微服务运行时,内嵌 Jetty 12.1.5 |
| juneau-rest-client | 9.2.0 | REST 客户端(演示工程引入) |
| juneau-marshall | 9.2.0 | 序列化核心(示例构建服务引入) |
版本事实(实测 Maven Central):
- 最新稳定版为 9.2.0;聚合包
juneau-all最后版本为 9.1.0(9.2.0 起停发),juneau-marshall-rdf最后版本为 9.0-B1——编排文件中不要引用这两个构件的 9.2.0 版本。
四、部署步骤
4.1 准备部署目录与工程文件
创建目录:
mkdir -p /opt/data/quzuo/juneau-compose/juneau-demo/src/main/java/com/example
mkdir -p /opt/data/quzuo/juneau-compose/juneau-rest/src/main/java/com/example
juneau-demo 工程(构建型服务,引入 juneau-marshall 9.2.0)。
/opt/data/quzuo/juneau-compose/juneau-demo/pom.xml:
<?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>
<groupId>com.example</groupId>
<artifactId>juneau-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-marshall</artifactId>
<version>9.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.MarshallDemo</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
/opt/data/quzuo/juneau-compose/juneau-demo/src/main/java/com/example/MarshallDemo.java(完整源码):
package com.example;
import java.util.*;
import org.apache.juneau.BeanSession;
import org.apache.juneau.annotation.Bean;
import org.apache.juneau.annotation.Swap;
import org.apache.juneau.swap.StringSwap;
import org.apache.juneau.json.*;
import org.apache.juneau.xml.*;
import org.apache.juneau.html.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.soap.*;
public class MarshallDemo {
@Bean(typeName="Pet", sort=true)
public static class Pet {
private int id;
private String name;
private List<String> tags;
private double price;
private Status status;
private Date soldDate;
public enum Status { AVAILABLE, PENDING, SOLD }
public Pet() {
}
public Pet(int id, String name, List<String> tags, double price, Status status, Date soldDate) {
this.id = id;
this.name = name;
this.tags = tags;
this.price = price;
this.status = status;
this.soldDate = soldDate;
}
public int getId() { return id; }
public String getName() { return name; }
public List<String> getTags() { return tags; }
public double getPrice() { return price; }
public Status getStatus() { return status; }
public Date getSoldDate() { return soldDate; }
public void setId(int id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setTags(List<String> tags) { this.tags = tags; }
public void setPrice(double price) { this.price = price; }
public void setStatus(Status status) { this.status = status; }
public void setSoldDate(Date soldDate) { this.soldDate = soldDate; }
}
/** 自定义 Swap:姓名首字母保留、其余打码 */
public static class NameMaskSwap extends StringSwap<String> {
@Override
public String swap(BeanSession session, String o) {
return o.charAt(0) + "**";
}
}
@Bean(typeName="Owner")
public static class Owner {
@Swap(NameMaskSwap.class)
public String realName = "zhangsan";
public int level = 3;
}
public static void main(String[] args) throws Exception {
Pet pet = new Pet(1001, "beibei",
Arrays.asList("golden", "young"), 599.0,
Pet.Status.PENDING, new Date(0L));
System.out.println("=== [1] JsonSerializer 可读格式(ws) ===");
JsonSerializer j1 = JsonSerializer.create().ws().build();
System.out.println(j1.serialize(pet));
System.out.println("=== [2] JsonSerializer 紧凑格式 + 单引号(sq) ===");
JsonSerializer j2 = JsonSerializer.create().sq().build();
System.out.println(j2.serialize(pet));
System.out.println("=== [3] XmlSerializer 命名空间格式 ===");
XmlSerializer x1 = XmlSerializer.create().ns().ws().build();
System.out.println(x1.serialize(pet));
System.out.println("=== [4] HtmlSerializer ===");
HtmlSerializer h1 = HtmlSerializer.create().ws().build();
System.out.println(h1.serialize(pet));
System.out.println("=== [5] UonSerializer ===");
UonSerializer u1 = UonSerializer.create().build();
System.out.println(u1.serialize(pet));
System.out.println("=== [6] UrlEncodingSerializer ===");
UrlEncodingSerializer ue1 = UrlEncodingSerializer.create().build();
System.out.println(ue1.serialize(pet));
System.out.println("=== [7] SoapXmlSerializer ===");
SoapXmlSerializer s1 = SoapXmlSerializer.create().build();
System.out.println(s1.serialize(pet));
System.out.println("=== [8] Map/List 序列化 ===");
Map<String,Object> m = new LinkedHashMap<>();
m.put("count", 2);
m.put("names", Arrays.asList("beibei", "lele"));
System.out.println(j1.serialize(m));
System.out.println("=== [9] @Swap 注解(NameMaskSwap) ===");
System.out.println(j1.serialize(new Owner()));
System.out.println("=== [10] JsonParser 反序列化(回读校验) ===");
JsonParser parser = JsonParser.create().build();
String json = j1.serialize(pet);
Pet back = parser.parse(json, Pet.class);
System.out.println("回读 id=" + back.getId() + " name=" + back.getName()
+ " tags=" + back.getTags() + " price=" + back.getPrice()
+ " status=" + back.getStatus() + " soldDate=" + back.getSoldDate());
System.out.println("=== [11] XmlParser 反序列化(回读校验) ===");
XmlParser xp = XmlParser.create().build();
Pet back2 = xp.parse(x1.serialize(pet), Pet.class);
System.out.println("回读 id=" + back2.getId() + " name=" + back2.getName() + " status=" + back2.getStatus());
System.out.println("=== [12] _type 字典类型(@Bean typeName + addRootType) ===");
String withType = JsonSerializer.create().ws().addRootType().build().serialize(pet);
System.out.println(withType);
Pet back3 = parser.parse(withType, Pet.class);
System.out.println("按 _type 还原: id=" + back3.getId() + " name=" + back3.getName());
}
}
juneau-rest 工程(常驻服务,引入 juneau-microservice-jetty 9.2.0 与 juneau-rest-client 9.2.0)。
/opt/data/quzuo/juneau-compose/juneau-rest/pom.xml:
<?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>
<groupId>com.example</groupId>
<artifactId>juneau-rest</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-microservice-jetty</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-client</artifactId>
<version>9.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.App</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
/opt/data/quzuo/juneau-compose/juneau-rest/src/main/java/com/example/App.java:
package com.example;
import org.apache.juneau.microservice.jetty.*;
public class App {
public static void main(String[] args) throws Exception {
JettyMicroservice
.create()
.args(args)
.servlet(RootRest.class)
.build()
.start()
.join();
}
}
/opt/data/quzuo/juneau-compose/juneau-rest/src/main/java/com/example/RootRest.java:
package com.example;
import java.util.*;
import org.apache.juneau.http.annotation.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.servlet.*;
@Rest(
title="Juneau 宠物商店示例服务",
path="/",
description="基于 juneau-microservice-jetty 的 REST 演示服务"
)
public class RootRest extends BasicRestServlet {
@RestGet(path="/hello", summary="返回问候字符串")
public String hello() {
return "Hello from Apache Juneau 9.2.0 REST";
}
@RestGet(path="/pet/{id}", summary="按 ID 查询宠物")
public Pet getPet(@Path("id") int id) {
return new Pet(id, "beibei", Arrays.asList("golden", "young"), 599.0, Pet.Status.PENDING);
}
@RestGet(path="/pets", summary="查询全部宠物")
public List<Pet> getPets() {
return Arrays.asList(
new Pet(1001, "beibei", Arrays.asList("golden", "young"), 599.0, Pet.Status.PENDING),
new Pet(1002, "lele", Arrays.asList("corgi"), 899.0, Pet.Status.AVAILABLE));
}
@RestGet(path="/greet", summary="查询参数问候")
public String greet(@Query(name="name", def="stranger") String name) {
return "Hello, " + name + "!";
}
@RestPost(path="/pet", summary="提交宠物信息并回显")
public Pet echoPet(@Content Pet pet) {
return pet;
}
}
/opt/data/quzuo/juneau-compose/juneau-rest/src/main/java/com/example/Pet.java:
package com.example;
import java.util.*;
import org.apache.juneau.annotation.Bean;
@Bean(typeName="Pet", sort=true)
public class Pet {
private int id;
private String name;
private List<String> tags;
private double price;
public enum Status { AVAILABLE, PENDING, SOLD }
private Status status;
public Pet() {
}
public Pet(int id, String name, List<String> tags, double price, Status status) {
this.id = id;
this.name = name;
this.tags = tags;
this.price = price;
this.status = status;
}
public int getId() { return id; }
public String getName() { return name; }
public List<String> getTags() { return tags; }
public double getPrice() { return price; }
public Status getStatus() { return status; }
public void setId(int id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setTags(List<String> tags) { this.tags = tags; }
public void setPrice(double price) { this.price = price; }
public void setStatus(Status status) { this.status = status; }
}
/opt/data/quzuo/juneau-compose/juneau-rest/jetty.xml(与 juneau-microservice-jetty 9.2.0 官方模板一致,端口变量 $S{availablePort,10000}):
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<Configure id="ExampleServer" class="org.eclipse.jetty.server.Server">
<Set name="connectors">
<Array type="org.eclipse.jetty.server.Connector">
<Item>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg>
<Ref refid="ExampleServer" />
</Arg>
<Set name="port">$S{availablePort,10000}</Set>
</New>
</Item>
</Array>
</Set>
<!-- Juneau adds its servlets in this ServletContextHandler -->
<New id="context" class="org.eclipse.jetty.ee11.servlet.ServletContextHandler">
<Set name="contextPath">/</Set>
<Set name="sessionHandler">
<New class="org.eclipse.jetty.ee11.servlet.SessionHandler" />
</Set>
</New>
<!-- Save ServletContextHandler for Juneau -->
<Call name="setAttribute">
<Arg>ServletContextHandler</Arg>
<Arg><Ref refid="context" /></Arg>
</Call>
<Set name="handler">
<New id="collection" class="org.eclipse.jetty.server.handler.ContextHandlerCollection">
<Arg>
<Array type="org.eclipse.jetty.server.handler.ContextHandler">
<Item>
<New class="org.eclipse.jetty.server.handler.ContextHandler">
<!-- This is setHandler(Supplier) -->
<Set name="handler"><Ref refid="context" /></Set>
</New>
</Item>
<Item>
<New class="org.eclipse.jetty.server.handler.ContextHandler">
<New class="org.eclipse.jetty.server.handler.DefaultHandler" />
</New>
</Item>
</Array>
</Arg>
</New>
</Set>
<Set name="requestLog">
<New id="RequestLogImpl" class="org.eclipse.jetty.server.CustomRequestLog">
<!-- Param 0: org.eclipse.jetty.server.RequestLogWriter -->
<Arg>
<New class="org.eclipse.jetty.server.RequestLogWriter">
<Set name="append">false</Set>
<Set name="filename"><Property name="jetty.logs" default="$S{Logging/logDir,logs}"/>/jetty-requests.log</Set>
<Set name="filenameDateFormat">yyyy_MM_dd</Set>
<Set name="retainDays">90</Set>
<Set name="timeZone">GMT</Set>
</New>
</Arg>
<!-- Param 1: String -->
<Arg>
<Get class="org.eclipse.jetty.server.CustomRequestLog" name="EXTENDED_NCSA_FORMAT" />
</Arg>
</New>
</Set>
<Get name="ThreadPool">
<Set name="minThreads" type="int">10</Set>
<Set name="maxThreads" type="int">100</Set>
<Set name="idleTimeout" type="int">60000</Set>
<Set name="detailedDump">true</Set>
</Get>
</Configure>
/opt/data/quzuo/juneau-compose/juneau-rest/demo.cfg:
# =============================================================================
# Juneau 微服务演示配置文件
# =============================================================================
[Logging]
logDir = logs
logFile = juneau-rest_%G_%M-%N.log
level = INFO
[Jetty]
port = 10000
resolveVars = true
maxThreads = 100
minThreads = 10
idleTimeout = 60000
4.2 编写 compose.yml
在 /opt/data/quzuo/juneau-compose/compose.yml 中编写编排(两个服务共用一个 m2cache 命名卷缓存 Maven 依赖):
services:
juneau-demo-build:
image: maven:3.9-eclipse-temurin-17
container_name: juneau-demo-build
working_dir: /build/juneau-demo
command: ["mvn", "package", "-DskipTests"]
volumes:
- /opt/data/quzuo/juneau-compose/juneau-demo:/build/juneau-demo
- m2cache:/root/.m2
juneau-rest:
image: maven:3.9-eclipse-temurin-17
container_name: juneau-rest-app
working_dir: /app/juneau-rest
command: ["java", "-Xms256m", "-Xmx512m", "-Dfile.encoding=UTF-8", "-jar", "target/juneau-rest-1.0.0.jar", "--cfg=/app/juneau-rest/demo.cfg"]
volumes:
- /opt/data/quzuo/juneau-compose/juneau-rest:/app/juneau-rest
- m2cache:/root/.m2
ports:
- "10000:10000"
depends_on:
juneau-demo-build:
condition: service_completed_successfully
volumes:
m2cache:
💡 说明:
juneau-rest通过 bind 挂载直接运行工程内已构建的target/juneau-rest-1.0.0.jar(首次部署前在本机或镜像内先执行一次mvn package);depends_on的service_completed_successfully保证构建服务成功退出后再启动常驻服务。
4.3 首次构建与启动
cd /opt/data/quzuo/juneau-compose
docker compose up -d
真实回显(完整):
Network juneau-compose_default Creating
Network juneau-compose_default Creating
Network juneau-compose_default Created
Network juneau-compose_default Created
Container juneau-demo-build Creating
Container juneau-demo-build Created
Container juneau-rest-app Creating
Container juneau-rest-app Created
Container juneau-demo-build Starting
Container juneau-demo-build Started
Container juneau-demo-build Waiting
Container juneau-demo-build Exited
Container juneau-rest-app Starting
Container juneau-rest-app Started
juneau-demo-build Exited 后 juneau-rest-app Started 说明编排流程符合预期(构建型服务一次性退出,常驻型服务随后启动)。
4.4 查看服务状态
docker compose ps -a
真实回显:
NAME IMAGE SERVICE STATUS
juneau-demo-build maven:3.9-eclipse-temurin-17 juneau-demo-build Exited (0) 50 seconds ago
juneau-rest-app maven:3.9-eclipse-temurin-17 juneau-rest Up 49 seconds
再确认构建容器退出码:
docker inspect juneau-demo-build --format "{{.State.Status}} exit={{.State.ExitCode}}"
真实回显:
exited exit=0
4.5 查看构建服务日志
docker compose logs juneau-demo-build
真实回显(关键行):
juneau-demo-build | [INFO] Building jar: /build/juneau-demo/target/juneau-demo-1.0.0.jar
juneau-demo-build | [INFO] Including org.apache.juneau:juneau-marshall:jar:9.2.0 in the shaded jar.
juneau-demo-build | [INFO] Including org.apache.juneau:juneau-commons:jar:9.2.0 in the shaded jar.
juneau-demo-build | [INFO] Including org.apache.httpcomponents:httpcore:jar:4.4.16 in the shaded jar.
juneau-demo-build | [INFO] Replacing original artifact with shaded artifact.
juneau-demo-build | [INFO] Replacing /build/juneau-demo/target/juneau-demo-1.0.0.jar with /build/juneau-demo/target/juneau-demo-1.0.0-shaded.jar
juneau-demo-build | [INFO] BUILD SUCCESS
4.6 查看常驻服务日志
docker compose logs juneau-rest
真实回显(关键行):
juneau-rest-app | 2026-10-11 13:05:07.522:INFO :oeje11s.ServletContextHandler:main: Started oeje11s.ServletContextHandler@79dc5318{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.SessionHandler@8e50104{STARTED}}
juneau-rest-app | 2026-10-11 13:05:07.522:INFO :oejsh.ContextHandler:main: Started oejsh.ContextHandler@5a021cb9{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.SessionHandler@8e50104{STARTED}}
juneau-rest-app | 2026-10-11 13:05:07.523:INFO :oejsh.ContextHandler:main: Started oejsh.ContextHandler@51768776{ROOT,/,b=null,a=AVAILABLE}
juneau-rest-app | 2026-10-11 13:05:07.527:INFO :oejs.RequestLogWriter:main: Opened /app/juneau-rest/logs/jetty-requests.log
juneau-rest-app | 2026-10-11 13:05:07.540:INFO :oejs.AbstractConnector:main: Started oejs.ServerConnector@37d80fe7{HTTP/1.1, (http/1.1)}{0.0.0.0:10000}
juneau-rest-app | 2026-10-11 13:05:07.541:INFO :oejs.Server:main: Started oejs.Server@384fc774{STARTING}[12.1.5,sto=0] @806ms
4.7 curl 验证 REST 接口(宿主机直连映射端口)
GET /pet/1001(JSON):
curl -s -H "Accept: application/json" http://127.0.0.1:10000/pet/1001
真实回显:
{"id":1001,"name":"beibei","price":599.0,"status":"PENDING","tags":["golden","young"]}
GET /pet/1002(XML):
curl -s -H "Accept: text/xml" http://127.0.0.1:10000/pet/1002
真实回显:
<?xml version="1.0" encoding="UTF-8"?><Pet><id>1002</id><name>beibei</name><price>599.0</price><status>PENDING</status><tags><string>golden</string><string>young</string></tags></Pet>
POST /pet(JSON 提交回显):
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"id":4001,"name":"duoduo","tags":["husky"],"price":999.0,"status":"AVAILABLE"}' \
http://127.0.0.1:10000/pet
真实回显:
{"id":4001,"name":"duoduo","price":999.0,"status":"AVAILABLE","tags":["husky"]}
4.8 运行序列化示例(复用构建容器)
构建容器退出后仍可复用镜像执行 fat-jar:
docker compose run --rm juneau-demo-build \
java -Xms256m -Xmx256m -Dfile.encoding=UTF-8 -jar /build/juneau-demo/target/juneau-demo-1.0.0.jar
真实回显(节选):
=== [1] JsonSerializer 可读格式(ws) ===
{
"id": 1001,
"name": "beibei",
"price": 599.0,
"soldDate": "1970-01-01T00:00:00",
"status": "PENDING",
"tags": [
"golden",
"young"
]
}
=== [2] JsonSerializer 紧凑格式 + 单引号(sq) ===
{'id':1001,'name':'beibei','price':599.0,'soldDate':'1970-01-01T00:00:00','status':'PENDING','tags':['golden','young']}
=== [3] XmlSerializer 命名空间格式 ===
<Pet>
<id>1001</id>
<name>beibei</name>
<price>599.0</price>
<soldDate>1970-01-01T00:00:00</soldDate>
...
💡 完整 12 段回显见《Ubuntu 22.04 安装 Apache Juneau 指南》4.7 与《Apache Juneau 常用 API 与示例详解》。
4.9 等价的单条 docker 命令
不使用 Compose 时,等价操作为(映射 10000 端口、限制 JVM 堆 256–512MB):
docker run -d --name juneau-rest-app \
-p 10000:10000 \
-v /opt/data/quzuo/juneau-compose/juneau-rest:/app/juneau-rest \
maven:3.9-eclipse-temurin-17 \
java -Xms256m -Xmx512m -Dfile.encoding=UTF-8 \
-jar /app/juneau-rest/target/juneau-rest-1.0.0.jar --cfg=/app/juneau-rest/demo.cfg
五、配置说明
- compose.yml 关键字段:
image:统一使用maven:3.9-eclipse-temurin-17,构建(mvn package)与运行(java -jar)共用同一镜像,保证 JDK 版本一致(17)。command:构建服务执行mvn package -DskipTests;常驻服务执行java -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -jar target/juneau-rest-1.0.0.jar --cfg=/app/juneau-rest/demo.cfg(堆内存限定 256–512MB)。volumes:工程目录 bind 挂载进容器;m2cache命名卷持久化/root/.m2,避免每次构建重复下载依赖。ports:10000:10000将微服务端口映射到宿主。depends_on.condition: service_completed_successfully:构建服务成功退出(Exited 0)后才启动常驻服务。
- 微服务配置:demo.cfg 的
[Jetty]/[Logging]段与 jetty.xml 的$S{availablePort,10000}变量,语义同《Apache Juneau 配置参数详解》;注意resolveVars = true必须保留。 - 日志落盘:REST 容器把
logs/写在 bind 挂载的工程目录下,宿主机可直接查看/opt/data/quzuo/juneau-compose/juneau-rest/logs/jetty-requests.log。
六、注意事项
- 构建服务
Exited (0)是正常状态:juneau-demo-build是一次性构建任务,docker compose ps显示 Exited 属预期;以inspect ... exit=0与BUILD SUCCESS日志为准。 - 首次启动耗时:首启时
m2cache卷为空,maven 需下载全部依赖(实测含 juneau/jetty/httpcore 等),常驻服务要等构建完成(depends_on)才启动;观察进度用docker compose logs -f juneau-demo-build。 juneau-all:9.2.0、juneau-marshall-rdf:9.2.0不存在:Maven Central 无这两个构件的 9.2.0 版本,依赖写错会导致构建容器退出非 0。- 端口占用:宿主 10000 被其它进程占用时
docker compose up会绑定失败并报port is already allocated,需先释放端口。 - 运行中不可重建挂载的 jar:常驻服务运行期间重跑
mvn package覆盖target/*.jar,会引发 JVM 懒加载NoClassDefFoundError;先docker compose restart juneau-rest或先 down 再构建。 - Bean 无参构造器:
MarshallDemo.Pet与Pet都显式保留了公共无参构造器,否则反序列化/@Content参数绑定会失败。
七、常见问题
Q1:docker compose up -d 卡在 Waiting 后常驻服务未启动
说明 juneau-demo-build 退出码非 0(如依赖下载失败、编译错误)。执行 docker compose logs juneau-demo-build 查看 BUILD FAILURE 原因;修好工程后 docker compose up -d 重跑。
Q2:构建时 Could not find artifact org.apache.juneau:juneau-all:jar:9.2.0
9.2.0 起该聚合包停发(实测最后版本 9.1.0),把依赖改为 juneau-marshall:9.2.0;同理不要引用 juneau-marshall-rdf(最后版本 9.0-B1)。
Q3:启动报 NumberFormatException: For input string: "$S{availablePort,10000}"
demo.cfg 的 [Jetty] 段缺 resolveVars = true,补上后重启常驻服务。
Q4:宿主 curl 返回 Connection refused
常驻服务尚未启动完成(首启需等待构建服务退出+Jetty 初始化,实测约 10 秒)。用 docker compose logs juneau-rest 确认出现 Started oejs.ServerConnector ... {0.0.0.0:10000} 后再验证。
Q5:请求 Accept: application/xml 返回 406
Juneau 9.2.0 的 XML 媒体类型是 text/xml(实测 application/xml 会返回 406 并在正文列出支持类型表),改用 text/xml 即可。
八、停止与卸载
停止并移除编排资源(保留 m2cache 依赖缓存):
cd /opt/data/quzuo/juneau-compose
docker compose down
真实回显(完整):
Container juneau-rest-app Stopping
Container juneau-rest-app Stopped
Container juneau-rest-app Removing
Container juneau-rest-app Removed
Container juneau-demo-build Stopping
Container juneau-demo-build Stopped
Container juneau-demo-build Removing
Container juneau-demo-build Removed
Network juneau-compose_default Removing
Network juneau-compose_default Removed
连同 m2cache 命名卷一起清理(下次构建会重新下载依赖):
docker compose down -v
删除部署目录与镜像:
rm -rf /opt/data/quzuo/juneau-compose
docker rmi maven:3.9-eclipse-temurin-17
九、参考链接
- Apache Juneau 官网:https://juneau.apache.org/
- Juneau 文档(_microservice-jetty Basics):https://juneau.apache.org/docs/topics/JuneauMicroserviceJettyBasics
- Maven 官方镜像:maven - Docker Image(https://hub.docker.com/_/maven)
- Maven Central:juneau-microservice-jetty(https://repo1.maven.org/maven2/org/apache/juneau/juneau-microservice-jetty/)
- Compose 文件参考:https://docs.docker.com/reference/compose-file/