Ubuntu 22.04 安装 Apache Juneau 指南

在 Ubuntu 22.04 上搭建 JDK 17 + Maven 环境,引入 Apache Juneau 9.2.0 依赖,实测 POJO 多格式序列化与 REST 微服务的编译、启动、curl 验证全流程。

Apache JuneauApache Juneau2026/10/12

Ubuntu 22.04 安装 Apache Juneau 指南

目标:Apache Juneau 是 Java 侧 POJO 序列化/反序列化与 REST 微服务框架,并非可独立安装的二进制发行版,因此本文以「集成/环境搭建」形态呈现——从零安装 JDK 17 与 Maven 3.6.3,用 Maven 引入 juneau-marshall 9.2.0 与 juneau-microservice-jetty 9.2.0 依赖,真实编译运行序列化示例与 REST 微服务并采集真实回显。已在 Ubuntu 22.04.5 LTS(x86_64)实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS,Kernel 6.8.0-138-generic,架构 x86_64(amd64)
  • OpenJDK 17:17.0.20.1(2026-08-18)
  • Apache Maven 3.6.3(Maven home:/usr/share/maven)
  • Apache Juneau:9.2.0(当前最新稳定版),涉及构件 juneau-marshall、juneau-microservice-jetty、juneau-rest-server、juneau-rest-client
  • 内嵌 Jetty:12.1.5(juneau-microservice-jetty 传递依赖)
  • 示例工作目录:/opt/juneau-demo(序列化示例)、/opt/juneau-rest(REST 微服务)
  • REST 服务监听端口:10000(由 demo.cfg 中 Jetty/port 指定)
  • 微服务日志目录:/opt/juneau-rest/logs

二、前置条件

  • 已具备 root 或 sudo 权限,可执行 apt-get install、wget。
  • 服务器可联网访问 Maven 中央仓库(本文实测使用阿里云 Maven 镜像 https://maven.aliyun.com/repository/central,见 4.3)。
  • 防火墙/安全组放行 10000 端口(REST 接口验证用)。

三、版本选型说明

Juneau 9.x 以 jar 形式按构件提供(Maven 依赖,无独立安装包):

构件Maven 坐标(groupId : artifactId)本次实测版本说明
序列化核心org.apache.juneau : juneau-marshall9.2.0JSON/JSON5/XML/HTML/UON/URL-ENC/OpenAPI/MsgPack/SoapXml/PlainText/CSV 全部序列化与解析器
REST 服务端org.apache.juneau : juneau-rest-server9.2.0@Rest/@RestGet/@RestPost 注解与 RestServlet 体系
REST 客户端org.apache.juneau : juneau-rest-client9.2.0RestClient 流式调用与响应断言
Jetty 微服务org.apache.juneau : juneau-microservice-jetty9.2.0可执行 REST 微服务,内嵌 Jetty 12.1.5

版本事实(实测 Maven Central maven-metadata.xml):

  • 最新稳定版为 9.2.0(juneau-rest-server 元数据 latest/release 均为 9.2.0)。
  • 聚合包 juneau-all 最后版本为 9.1.0,9.2.0 起不再发布——9.2.0 工程请改用 juneau-marshall。
  • RDF 支持构件 juneau-marshall-rdf 最后版本为 9.0-B1,9.2.0 中不含 RDF 序列化(实测 9.2.0 服务端支持媒体类型列表中亦无 RDF 类型)。

选型建议:

  • 只做 POJO 序列化/解析:依赖 juneau-marshall 单个构件即可。
  • 需要可执行 REST 微服务:直接依赖 juneau-microservice-jetty(它已传递引入 juneau-rest-server、juneau-config 与 Jetty 12)。
  • 调用 Juneau REST 服务:依赖 juneau-rest-client。

序列化核心依赖解析(实测 mvn dependency:resolve 输出):

[INFO]    org.apache.juneau:juneau-marshall:jar:9.2.0:compile
[INFO] BUILD SUCCESS

REST 微服务关键依赖(实测 mvn dependency:resolve 输出节选):

[INFO]    org.apache.juneau:juneau-microservice-jetty:jar:9.2.0:compile
[INFO]    org.apache.juneau:juneau-microservice-core:jar:9.2.0:compile
[INFO]    org.apache.juneau:juneau-rest-server:jar:9.2.0:compile
[INFO]    org.apache.juneau:juneau-config:jar:9.2.0:compile
[INFO]    org.eclipse.jetty:jetty-server:jar:12.1.5:compile
[INFO]    org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:12.1.5:compile

四、部署步骤(集成与编译运行)

4.1 安装 JDK 17

Juneau 9.2.0 官方示例按 Java 17 构建,通过 apt 安装:

apt-get update -y
apt-get install -y openjdk-17-jdk

验证安装:

java -version

真实回显:

openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)

确认 JDK 目录:

ls /usr/lib/jvm/

真实回显:

default-java  java-1.11.0-openjdk-amd64  java-1.17.0-openjdk-amd64  java-11-openjdk-amd64  java-17-openjdk-amd64  openjdk-17

4.2 安装 Maven

apt-get install -y maven

验证版本:

mvn -version

真实回显:

Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"

4.3 配置 Maven 镜像加速

下载依赖耗时明显时,配置阿里云中央仓库镜像以加速(实测节点即走该镜像):

mkdir -p /root/.m2

在 /root/.m2/settings.xml 中写入镜像配置:

<settings>
  <mirrors>
    <mirror>
      <id>aliyun</id>
      <mirrorOf>central</mirrorOf>
      <name>Aliyun Maven Central Mirror</name>
      <url>https://maven.aliyun.com/repository/central</url>
    </mirror>
  </mirrors>
</settings>

4.4 创建序列化演示工程并引入依赖

建立目录结构:

mkdir -p /opt/juneau-demo/src/main/java/com/example

创建工程描述文件 /opt/juneau-demo/pom.xml,引入 juneau-marshall:9.2.0,并用 maven-shade-plugin 打可执行 fat-jar:

<?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/juneau-demo/src/main/java/com/example/MarshallDemo.java(含 @Bean/@Swap 注解的 Pet/Owner Bean 与 12 段序列化/反序列化演示,完整源码与逐段回显见《Apache Juneau 常用 API 与示例详解》)。

4.5 解析依赖(验证真实版本)

cd /opt/juneau-demo
mvn dependency:resolve -DincludeArtifactIds=juneau-marshall

真实回显(关键行):

[INFO] --- maven-dependency-plugin:2.8:resolve (default-cli) @ juneau-demo ---
[INFO]    org.apache.juneau:juneau-marshall:jar:9.2.0:compile
[INFO] BUILD SUCCESS

4.6 编译打包

cd /opt/juneau-demo
mvn package

真实回显(关键行):

[INFO] --- maven-shade-plugin:3.5.2:shade (default) @ juneau-demo ---
[INFO] Including org.apache.juneau:juneau-marshall:jar:9.2.0 in the shaded jar.
[INFO] Including org.apache.juneau:juneau-commons:jar:9.2.0 in the shaded jar.
[INFO] Including org.apache.httpcomponents:httpcore:jar:4.4.16 in the shaded jar.
[INFO] Replacing /opt/juneau-demo/target/juneau-demo-1.0.0.jar with /opt/juneau-demo/target/juneau-demo-1.0.0-shaded.jar
[INFO] BUILD SUCCESS

产物:/opt/juneau-demo/target/juneau-demo-1.0.0.jar(实测 2474590 字节)。

4.7 运行序列化示例并验证

cd /opt/juneau-demo
java -Xms256m -Xmx256m -Xmn128m -Dfile.encoding=UTF-8 -jar 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>
        <status>PENDING</status>
        <tags>
                <string>golden</string>
                <string>young</string>
        </tags>
</Pet>

=== [4] HtmlSerializer ===
<table>
        <tr>
                <td>id</td>
                <td>1001</td>
        </tr>
        <tr>
                <td>name</td>
                <td>beibei</td>
        </tr>
        <tr>
                <td>price</td>
                <td>599.0</td>
        </tr>
        <tr>
                <td>soldDate</td>
                <td>1970-01-01T00:00:00</td>
        </tr>
        <tr>
                <td>status</td>
                <td>PENDING</td>
        </tr>
        <tr>
                <td>tags</td>
                <td>
                        <ul>
                                <li>golden</li>
                                <li>young</li>
                        </ul>
                </td>
        </tr>
</table>

=== [5] UonSerializer ===
(id=1001,name=beibei,price=599.0,soldDate=1970-01-01T00:00:00,status=PENDING,tags=@(golden,young))
=== [6] UrlEncodingSerializer ===
id=1001&name=beibei&price=599.0&soldDate=1970-01-01T00:00:00&status=PENDING&tags=@(golden,young)
=== [7] SoapXmlSerializer ===
<?xml version="1.0" encoding="UTF-8"?><Envelope soap="http://www.w3.org/2003/05/soap-envelope"><Body><Pet><id>1001</id><name>beibei</name><price>599.0</price><soldDate>1970-01-01T00:00:00</soldDate><status>PENDING</status><tags><string>golden</string><string>young</string></tags></Pet></Body></Envelope>
=== [8] Map/List 序列化 ===
{
        "count": 2,
        "names": [
                "beibei",
                "lele"
        ]
}
=== [9] @Swap 注解(NameMaskSwap) ===
{
        "level": 3,
        "realName": "z**"
}
=== [10] JsonParser 反序列化(回读校验) ===
回读 id=1001 name=beibei tags=['golden','young'] price=599.0 status=PENDING soldDate=Thu Jan 01 00:00:00 GMT 1970
=== [11] XmlParser 反序列化(回读校验) ===
回读 id=1001 name=beibei status=PENDING
=== [12] _type 字典类型(@Bean typeName + addRootType) ===
{
        "_type": "Pet",
        "id": 1001,
        "name": "beibei",
        "price": 599.0,
        "soldDate": "1970-01-01T00:00:00",
        "status": "PENDING",
        "tags": [
                "golden",
                "young"
        ]
}
按 _type 还原: id=1001 name=beibei

💡 说明:java.util.Date 被 Juneau 内置交换器默认转成 1970-01-01T00:00:00 形式;缩进为 Tab;第 9 段验证 @Swap 生效(zhangsan → z**)。

4.8 创建 REST 微服务工程

建立目录:

mkdir -p /opt/juneau-rest/src/main/java/com/example

创建 /opt/juneau-rest/pom.xml,引入 juneau-microservice-jetty:9.2.0 与 juneau-rest-client:9.2.0:

<?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/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();
    }
}

创建 REST 资源类 /opt/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;
    }
}

创建数据 Bean /opt/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; }
}

创建 Jetty 配置 /opt/juneau-rest/jetty.xml(端口变量 $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/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

⚠️ 注意:resolveVars = true 必须保留——它控制 jetty.xml 中 $S{...} 变量是否被解析,缺失时启动直接失败(见七、常见问题 Q5)。

4.9 编译并启动 REST 微服务

cd /opt/juneau-rest
mvn package

真实回显(关键行):

[INFO] Building jar: /opt/juneau-rest/target/juneau-rest-1.0.0.jar
[INFO] Including org.apache.juneau:juneau-microservice-jetty:jar:9.2.0 in the shaded jar.
[INFO] Including org.apache.juneau:juneau-microservice-core:jar:9.2.0 in the shaded jar.
[INFO] Including org.apache.juneau:juneau-rest-server:jar:9.2.0 in the shaded jar.
[INFO] BUILD SUCCESS

产物:/opt/juneau-rest/target/juneau-rest-1.0.0.jar(实测 9119409 字节)。

后台启动(堆内存限制 256–512MB):

cd /opt/juneau-rest
nohup java -Xms256m -Xmx512m -Dfile.encoding=UTF-8 \
  -jar target/juneau-rest-1.0.0.jar --cfg=/opt/juneau-rest/demo.cfg > /tmp/rest-run.log 2>&1 &

启动日志真实回显(完整):

2026-10-11 13:00:06.463:INFO :oejs.Server:main: jetty-12.1.5; built: 2025-12-03T22:18:24.732Z; git: 4905d09d7b6801e792d7b73946cf7b66e4be25d6; jvm 17.0.20.1+1-1-22.04-Ubuntu
2026-10-11 13:00:06.495:INFO :oejs.DefaultSessionIdManager:main: Session workerName=node0
2026-10-11 13:00:06.508:INFO :oejsh.ContextHandler:main: Started oeje11s.ServletContextHandler@1115ec15{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.SessionHandler@82ea68c{STARTED}}
2026-10-11 13:00:07.044:INFO :oeje11s.ServletContextHandler:main: Started oeje11s.ServletContextHandler@1115ec15{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.SessionHandler@82ea68c{STARTED}}
2026-10-11 13:00:07.044:INFO :oejsh.ContextHandler:main: Started oejsh.ContextHandler@677dbd89{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.ServletContextHandler@1115ec15{ROOT,/,b=null,a=AVAILABLE,h=oeje11s.SessionHandler@82ea68c{STARTED}}}
2026-10-11 13:00:07.044:INFO :oejsh.ContextHandler:main: Started oejsh.ContextHandler@fff25f1{ROOT,/,b=null,a=AVAILABLE}
2026-10-11 13:00:07.047:INFO :oejs.RequestLogWriter:main: Opened /opt/juneau-rest/logs/jetty-requests.log
2026-10-11 13:00:07.056:INFO :oejs.AbstractConnector:main: Started oejs.ServerConnector@5a4bef8{HTTP/1.1, (http/1.1)}{0.0.0.0:10000}
2026-10-11 13:00:07.057:INFO :oejs.Server:main: Started oejs.Server@40bffbca{STARTING}[12.1.5,sto=0] @596ms

看到 Started oejs.ServerConnector ... {0.0.0.0:10000} 即表示服务已在 10000 端口监听。

4.10 curl 验证 REST 接口

GET /hello(带响应头):

curl -s -i http://127.0.0.1:10000/hello

真实回显:

HTTP/1.1 200 OK
Server: Jetty(12.1.5)
Date: Sun, 11 Oct 2026 12:43:54 GMT
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked

<html><head><style>@import "/htdocs/themes/devops.css";div.data * {white-space:nowrap;} </style><script></script></head><body><header><h1>Juneau 宠物商店示例服务</h1> <h2>返回问候字符串</h2></header><nav><ol><li><a href="/">up</a></li><li><a href="/api">api</a></li><li><a href="/stats">stats</a></li></ol></nav><section><article><div class="outerdata"><div class="data" id="data"><string>Hello from Apache Juneau 9.2.0 REST</string></div></div></article></section></body></html>

💡 说明:curl 不带 Accept 头时(等价 */*),Juneau 默认返回带页面包装的 HTML;想拿纯数据需显式指定 Accept。

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/1001(显式 XML,注意是 text/xml):

curl -s -i -H "Accept: text/xml" http://127.0.0.1:10000/pet/1001

真实回显:

HTTP/1.1 200 OK
Server: Jetty(12.1.5)
Date: Sun, 11 Oct 2026 12:44:23 GMT
Content-Type: text/xml;charset=utf-8
Transfer-Encoding: chunked

<?xml version="1.0" encoding="UTF-8"?><Pet><id>1001</id><name>beibei</name><price>599.0</price><status>PENDING</status><tags><string>golden</string><string>young</string></tags></Pet>

GET /greet 查询参数:

curl -s -H "Accept: application/json" "http://127.0.0.1:10000/greet?name=juneau"

真实回显:

"Hello, juneau!"

POST /pet(JSON 提交回显):

curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"id":2002,"name":"huanhuan","tags":["husky"],"price":1299.0,"status":"AVAILABLE"}' \
  http://127.0.0.1:10000/pet

真实回显:

{"id":2002,"name":"huanhuan","price":1299.0,"status":"AVAILABLE","tags":["husky"]}

运行统计端点(/stats,JSON 节选):

curl -s -H "Accept: application/json" http://127.0.0.1:10000/stats

真实回显(节选):

{"startTime":"2026-10-11T12:43:29.331681534Z","upTime":"0h:00m:54s","methodStats":[{"avgTime":0,"errors":0,"guid":3916378536949704445,"maxTime":0,"method":"public void org.apache.juneau.rest.servlet.BasicRestServlet.error()","minTime":0,"running":0,"runs":0,"totalTime":0},{"avgTime":0,"errors":0,"guid":-5856031140523421306,"maxTime":0,"method":"public com.example.Pet com.example.RootRest.getPet(int)","minTime":0,"running":0,"runs":5,"totalTime":0}, ...]}

访问日志(/opt/juneau-rest/logs/jetty-requests.log)真实回显:

127.0.0.1 - - [11/Oct/2026:12:53:09 +0000] "GET /hello HTTP/1.1" 200 37 "" "curl/7.81.0"

4.11 systemd 托管(生产推荐)

⚠️ 本节为真实服务器上的常驻托管写法,要求宿主机带 systemd(如 Ubuntu 22.04 真机)。以下命令在 systemd 机器上执行,未附回显(托管方式与 nohup 等效,验证以 4.10 的 curl 为准)。

创建 /etc/systemd/system/juneau-rest.service:

[Unit]
Description=Apache Juneau REST Microservice
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/juneau-rest
ExecStart=/usr/bin/java -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -jar /opt/juneau-rest/target/juneau-rest-1.0.0.jar --cfg=/opt/juneau-rest/demo.cfg
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

逐条执行(每条命令仅针对一个单元):

# 重载单元定义
systemctl daemon-reload

# 设置开机自启
systemctl enable juneau-rest

# 启动服务
systemctl start juneau-rest

# 查看运行状态
systemctl is-active juneau-rest

⚠️ 若 10000 端口已被 nohup 方式启动的旧进程占用,先结束旧进程再 systemctl start juneau-rest,否则服务会落在其它可用端口(见七、常见问题 Q6)。

五、配置说明

  • JVM 堆内存:限定 256–512MB。nohup 方式直接写在启动命令;systemd 方式写在 ExecStart 行。实测生效验证(jps -v):
8846 juneau-rest-1.0.0.jar -Xms256m -Xmx512m -Dfile.encoding=UTF-8
  • 微服务配置(demo.cfg):[Jetty] 段的 port(监听端口)、resolveVars(jetty.xml 变量解析开关)、maxThreads/minThreads/idleTimeout(线程池);[Logging] 段的 logDir/logFile/level。完整参数语义见《Apache Juneau 配置参数详解》。
  • Jetty 配置(jetty.xml):放在工作目录(与启动命令的当前目录一致)或 classpath 根下,Juneau 自动加载;$S{availablePort,10000} 引用微服务挑选出的可用端口。
  • 构建配置:maven-shade-plugin 打 fat-jar;fat-jar 已合并全部依赖,java -jar 直接运行。
  • 镜像加速:/root/.m2/settings.xml 指向阿里云中央仓库镜像(见 4.3)。
  • REST 资源注解:@Rest 声明资源与标题,@RestGet/@RestPost 声明方法,@Path/@Query/@Content 绑定入参;详见《Apache Juneau 配置参数详解》。

六、注意事项

  • Bean 必须有公共无参构造器:否则反序列化报 ParseException: Class could not be instantiated(见七、常见问题 Q4)。
  • 9.2.0 的注解包名:@Bean/@Swap 在 org.apache.juneau.annotation 包(旧版本在 org.apache.juneau 根包);POST 请求体注解是 @Content(旧版本为 @Body,9.2.0 已无该注解)。
  • XML 媒体类型是 text/xml:请求 Accept: application/xml 会得到 406,并在报错信息中列出全部支持类型(见七、常见问题 Q7)。
  • 中文与编码:启动命令加 -Dfile.encoding=UTF-8;响应默认 charset=utf-8。
  • juneau-all 与 RDF 构件在 9.2.0 不存在:不要按旧教程引 juneau-all:9.2.0 或 juneau-marshall-rdf:9.2.0(见七、常见问题 Q1)。
  • fat-jar 运行中不可被重新打包覆盖:服务运行期间执行 mvn package 替换同名 jar,会让运行中的 JVM 在懒加载新类时报 NoClassDefFoundError(见七、常见问题 Q3)。

七、常见问题

Q1:依赖报 Could not find artifact org.apache.juneau:juneau-all:jar:9.2.0

juneau-all 聚合包 9.2.0 起停发(实测 Maven Central 最后版本 9.1.0)。把依赖改为 juneau-marshall:9.2.0 即可,它已合并全部序列化/解析能力。

Q2:Maven 下载依赖极慢或失败

配置 4.3 的阿里云镜像;仍失败时先 mvn -U dependency:resolve 强制刷新。注意阿里云镜像对个别新构件有同步延迟,可临时改用官方仓库 https://repo1.maven.org/maven2。

Q3:运行中服务报 NoClassDefFoundError: org/apache/juneau/StringRanges

fat-jar 被运行中进程外的重新打包覆盖,导致 JVM 懒加载类时读到了不一致的 jar。规范做法:先停止进程,再 mvn package,最后启动。

Q4:反序列化报 ParseException: Class '...Pet' could not be instantiated. Reason: 'null'

Bean 缺公共无参构造器。为 Bean 补一个 public Pet() {} 构造器即可(实测该报错在补齐后消失)。

Q5:启动报 NumberFormatException: For input string: "$S{availablePort,10000}"

jetty.xml 中的 $S{...} 变量未被解析——demo.cfg 的 [Jetty] 段缺 resolveVars = true,补上后重启即可(实测报错消失)。

Q6:配置的 10000 端口被占,服务却起来了,监听在其它端口

Jetty/port 指定的端口不可用时,Juneau 会自动挑选一个可用端口并写回系统属性 availablePort(实测端口被占时监听到了 36265)。先释放端口(或改 Jetty/port),再重启;以启动日志 Started oejs.ServerConnector ... {0.0.0.0:端口} 为准。

Q7:请求 Accept: application/xml 返回 HTTP 406: Not Acceptable

Juneau 9.2.0 的 XML 媒体类型是 text/xml。把 Accept 改为 text/xml 即可;406 报错正文会列出全部支持类型:

HTTP 406: Not Acceptable

Unsupported media-type in request header 'Accept': 'application/xml'
        Supported media-types: ['text/html','text/html+stripped','application/json','text/json','application/json5','text/json5','application/json+schema','text/json+schema','text/xml','text/uon','application/x-www-form-urlencoded','text/openapi','octal/msgpack','text/xml+soap','text/plain','text/csv']

Q8:访问不存在的路径返回 HTTP 500 而非 404

实测 9.2.0 中未匹配到任何方法的路径抛 NotFound,响应为:

HTTP 500: Internal Server Error

jakarta.servlet.ServletException: Invalid method response: 200
Caused by (ServletException): Invalid method response: 200
Caused by (NotFound): Java method matching path '/notexist' not found on resource 'com.example.RootRest'.

按报错正文里的 NotFound 定位即可,这是 9.2.0 的真实行为。

八、卸载方式

Juneau 是依赖库,卸载即清理工程与本地缓存:

# 停止微服务(nohup 方式启动的进程)
pkill -f juneau-rest-1.0.0.jar

# systemd 托管方式则逐条执行
systemctl stop juneau-rest
systemctl disable juneau-rest

# 删除示例工程目录
rm -rf /opt/juneau-demo /opt/juneau-rest

# 清理本地 Maven 仓库中 Juneau 相关缓存
rm -rf /root/.m2/repository/org/apache/juneau

如不再需要 JDK/Maven 环境:

apt-get remove -y openjdk-17-jdk maven

九、参考链接