Apache Juneau Docker Compose 部署指南

以 Maven+JDK 镜像构建与运行 Apache Juneau 9.2.0 演示工程,用 Compose 编排 juneau-marshall 构建服务与 juneau-microservice-jetty REST 常驻服务,真实启动、curl 验证与运维命令全流程。

Apache JuneauApache Juneau2026/10/12

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-jetty9.2.0REST 微服务运行时,内嵌 Jetty 12.1.5
juneau-rest-client9.2.0REST 客户端(演示工程引入)
juneau-marshall9.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

九、参考链接