Apache Juneau 常用 API 与示例详解

按 API 分节详解 Apache Juneau 9.2.0 的 JsonSerializer/JsonParser/XmlSerializer/HtmlSerializer/UonSerializer/@Bean/@Swap/RestServlet/RestClient/JettyMicroservice 用法,全部示例真实运行并附逐行回显。

Apache JuneauApache Juneau2026/10/12

Apache Juneau 常用 API 与示例详解

目标:按 API 分节梳理 Apache Juneau 9.2.0 的常用入口——序列化/解析器(JsonSerializer、JsonParser、XmlSerializer、HtmlSerializer、UonSerializer、UrlEncodingSerializer、SoapXmlSerializer)、Bean 注解(@Bean、@Swap)、REST 服务端(@Rest、BasicRestServlet、@RestGet/@RestPost)、REST 客户端(RestClient)与微服务入口(JettyMicroservice)。所有示例均真实编译运行,回显逐行照抄实测结果(环境:Ubuntu 22.04.5 LTS + JDK 17.0.20.1;REST 演示服务监听 10000 端口)。

一、API 概览

API所在构件用途9.2.0 关键变化
JsonSerializer / JsonParserjuneau-marshallPOJO ↔ JSONBuilder 移除 simple();@Bean 注解迁至 org.apache.juneau.annotation 包
XmlSerializer / XmlParserjuneau-marshallPOJO ↔ XML媒体类型为 text/xml
HtmlSerializerjuneau-marshallPOJO → HTML 表格服务端默认 */* 返回 HTML 页面
UonSerializer / UrlEncodingSerializerjuneau-marshallPOJO → UON / 表单编码—
SoapXmlSerializerjuneau-marshallPOJO → SOAP Envelope—
@Bean / @Swapjuneau-marshallBean 定制properties 为 String;@Body 更名 @Content(rest 系)
RestServlet / @Restjuneau-rest-serverREST 服务端@Rest 在 org.apache.juneau.rest.annotation 包
RestClientjuneau-rest-clientREST 客户端需 .json() 注册解析器才能 as() 反序列化
JettyMicroservicejuneau-microservice-jetty可执行微服务内嵌 Jetty 12.1.5

⚠️ 版本事实:9.2.0 起聚合包 juneau-all 停发(最后 9.1.0),RDF 构件 juneau-marshall-rdf 停更(最后 9.0-B1),本文全部示例使用 juneau-marshall/juneau-rest-server/juneau-rest-client/juneau-microservice-jetty 的 9.2.0。

二、环境准备与依赖引入

Maven 坐标(全部实测解析成功):

<dependency>
  <groupId>org.apache.juneau</groupId>
  <artifactId>juneau-marshall</artifactId>
  <version>9.2.0</version>
</dependency>
<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>

依赖解析真实回显:

[INFO]    org.apache.juneau:juneau-marshall:jar:9.2.0:compile
[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] BUILD SUCCESS

本文 3–6 节的示例主类 MarshallDemo 放在 /opt/juneau-demo(pom 与编译运行步骤见《Ubuntu 22.04 安装 Apache Juneau 指南》);7–9 节的 REST 工程放在 /opt/juneau-rest。Bean 基类 Pet:

@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;
    }

    // 省略全部 getter/setter: getId/setName/getTags/getPrice/getStatus/getSoldDate 及对应 setter
}

⚠️ Bean 必须带公共无参构造器,否则解析报 ParseException: Class could not be instantiated(实测)。

三、JsonSerializer 与 JsonParser

3.1 JsonSerializer 构造与序列化

// 可读格式(缩进换行)
JsonSerializer j1 = JsonSerializer.create().ws().build();
// 紧凑 + 单引号
JsonSerializer j2 = JsonSerializer.create().sq().build();
// 输出 _type 字典类型
JsonSerializer jt = JsonSerializer.create().ws().addRootType().build();

Pet pet = new Pet(1001, "beibei", Arrays.asList("golden", "young"), 599.0,
        Pet.Status.PENDING, new Date(0L));
String json = j1.serialize(pet);

实测回显:

=== [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']}

Map/List 也能直接序列化:

=== [8] Map/List 序列化 ===
{
        "count": 2,
        "names": [
                "beibei",
                "lele"
        ]
}

3.2 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());

实测回显:

=== [10] JsonParser 反序列化(回读校验) ===
回读 id=1001 name=beibei tags=['golden','young'] price=599.0 status=PENDING soldDate=Thu Jan 01 00:00:00 GMT 1970

未知属性默认严格报错,宽容解析用 ignoreUnknownBeanProperties():

=== [1] 默认 parser 遇到未知属性 ===
异常: Unknown property 'unknown' encountered while trying to parse into class 'com.example.ConfigDemo$Pet'
        At: line 1, column 19
=== [2] ignoreUnknownBeanProperties(true) ===
com.example.ConfigDemo$Pet@10a035a0

3.3 _type 字典类型

@Bean(typeName="Pet") + addRootType() 输出 _type,解析时按 _type 自动还原:

=== [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

四、XML、SOAP 与 HTML 序列化/解析

4.1 XmlSerializer / XmlParser

XmlSerializer x1 = XmlSerializer.create().ns().ws().build();
XmlParser xp = XmlParser.create().build();
String xml = x1.serialize(pet);
Pet back2 = xp.parse(xml, Pet.class);

实测回显:

=== [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>

=== [11] XmlParser 反序列化(回读校验) ===
回读 id=1001 name=beibei status=PENDING

4.2 SoapXmlSerializer

=== [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>

4.3 HtmlSerializer

=== [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>

五、UON 与 URL-ENC 序列化

UonSerializer u1 = UonSerializer.create().build();
UrlEncodingSerializer ue1 = UrlEncodingSerializer.create().build();

实测回显:

=== [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)

六、Bean 注解:@Bean 与 @Swap

/** 自定义 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;
}

实测回显:

=== [9] @Swap 注解(NameMaskSwap) ===
{
        "level": 3,
        "realName": "z**"
}

@Bean 常用参数(9.2.0 实测):typeName(字典类型)、sort(字母序输出)、properties(String 白名单,如 properties="id")。属性白名单实测:

=== [3] @Bean(properties="id") 限定序列化属性 ===
{"id":7}

⚠️ 9.2.0 的 @Bean/@Swap 在 org.apache.juneau.annotation 包;properties 是 String 类型而非数组。

💡 以上 3–6 节全部输出来自 /opt/juneau-demo 工程 MarshallDemo 的一次连续运行(12 段),命令:java -Xms256m -Xmx256m -Xmn128m -Dfile.encoding=UTF-8 -jar target/juneau-demo-1.0.0.jar。

七、RestServlet REST 服务端 API

7.1 @Rest 资源声明与方法注解

@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;
    }
}

要点:

  • BasicRestServlet 默认挂载全部序列化器/解析器(支持类型见 7.3 的 406 回显);
  • 请求体注解是 @Content(9.2.0,旧版 @Body 已删除);
  • @Query 的 def 提供默认值。

7.2 curl 实测

请求命令要点真实回显
GET /hello(默认 Accept)curl -s -i http://127.0.0.1:10000/helloHTTP/1.1 200 OK;Content-Type: text/html;charset=utf-8;正文为 HTML 页面包装,<string>Hello from Apache Juneau 9.2.0 REST</string>
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)curl -s -H "Accept: text/xml" http://127.0.0.1:10000/pet/1001<?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 /pet/1001(UON)curl -s -H "Accept: text/uon" http://127.0.0.1:10000/pet/1001(id=1001,name=beibei,price=599.0,status=PENDING,tags=@(golden,young))
GET /greetcurl -s -H "Accept: application/json" "http://127.0.0.1:10000/greet?name=juneau""Hello, juneau!"
GET /petscurl -s -H "Accept: application/json" http://127.0.0.1:10000/pets[{"id":1001,"name":"beibei","price":599.0,"status":"PENDING","tags":["golden","young"]},{"id":1002,"name":"lele","price":899.0,"status":"AVAILABLE","tags":["corgi"]}]
POST /petcurl -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"]}

GET /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>

7.3 内置端点:stats 与 api

BasicRestServlet 自带运行统计与 OpenAPI 文档端点:

### GET /stats(Accept: application/json,节选)
{"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}, ...]}
### GET /api(Accept: application/json,节选)
{"openapi":"3.0.1","info":{"title":"Juneau 宠物商店示例服务","version":"9.2.0","description":"基于 juneau-microservice-jetty 的 REST 演示服务"},"paths":{"/hello":{"get":{"summary":"返回问候字符串", ...}}}}

7.4 错误响应实测

不支持媒体类型(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']

未知路径(实测 9.2.0 返回 500 包裹 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'.

八、RestClient REST 客户端 API

8.1 构造与调用

RestClient client = RestClient.create().json().build();
try {
    // 1. GET 取 JSON 字符串
    RestResponse r1 = client.get("http://127.0.0.1:10000/pet/1001")
            .accept("application/json").run();
    System.out.println("状态码=" + r1.getStatusCode()
            + " Content-Type=" + r1.getContentType().map(Object::toString).orElse(""));
    System.out.println(r1.getContent().asString());

    // 2. GET 响应直接反序列化为 Pet
    RestResponse r2 = client.get("http://127.0.0.1:10000/pet/1001")
            .accept("application/json").run();
    Pet p = r2.getContent().as(Pet.class);
    System.out.println("id=" + p.getId() + " name=" + p.getName()
            + " tags=" + p.getTags() + " status=" + p.getStatus());

    // 3. POST 提交 POJO(自动序列化)
    Pet newPet = new Pet(2003, "duoduo", Arrays.asList("border collie"), 1099.0, Pet.Status.PENDING);
    RestResponse r3 = client.post("http://127.0.0.1:10000/pet", newPet)
            .accept("application/json").run();
    System.out.println("状态码=" + r3.getStatusCode());
    System.out.println(r3.getContent().asString());

    // 4. Accept: text/xml 内容协商
    RestResponse r4 = client.get("http://127.0.0.1:10000/pet/1001")
            .accept("text/xml").run();
    System.out.println("状态码=" + r4.getStatusCode()
            + " Content-Type=" + r4.getContentType().map(Object::toString).orElse(""));
    System.out.println(r4.getContent().asString());

    // 5. 断言式校验
    RestResponse r5 = client.get("http://127.0.0.1:10000/hello")
            .accept("application/json").run()
            .assertStatus(200)
            .assertContent("\"Hello from Apache Juneau 9.2.0 REST\"");
    System.out.println("断言通过, 状态码=" + r5.getStatusCode());
} finally {
    client.close();
}

8.2 实测回显(完整)

=== [1] GET /pet/1001 取 JSON 字符串 ===
状态码=200 Content-Type=application/json
{"id":1001,"name":"beibei","price":599.0,"status":"PENDING","tags":["golden","young"]}
=== [2] GET 响应直接反序列化为 Pet ===
id=1001 name=beibei tags=[golden, young] status=PENDING
=== [3] POST 提交 POJO(自动序列化) ===
状态码=200
{"id":2003,"name":"duoduo","price":1099.0,"status":"PENDING","tags":["border collie"]}
=== [4] Accept: text/xml 内容协商 ===
状态码=200 Content-Type=text/xml;charset=utf-8
<?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>
=== [5] 断言式校验(assertStatus/assertContent) ===
断言通过, 状态码=200

⚠️ 实测要点:

  1. RestClient.create() 不带 .json() 时,getContent().as(Pet.class) 报 Unsupported media-type in request header 'Content-Type': 'application/json'——Builder 必须注册对应解析器;
  2. POST 传 POJO 时按默认 JSON 序列化发送,服务端 @Content Pet 自动反序列化;
  3. assertContent 比较响应原文,JSON 化的字符串带引号。

九、JettyMicroservice 微服务 API

9.1 入口类

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();
    }
}
  • args(args) 透传命令行参数(支持 --cfg=/path/demo.cfg);
  • servlet(RootRest.class) 挂载 REST 资源;
  • start().join() 启动并阻塞主线程。

9.2 启动命令与日志

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

9.3 jetty.xml 与 demo.cfg

jetty.xml(端口变量)与 demo.cfg([Jetty] port/resolveVars/线程池、[Logging] 日志)完整内容见《Ubuntu 22.04 安装 Apache Juneau 指南》4.8,参数语义见《Apache Juneau 配置参数详解》五。

十、注意事项

  • Bean 无参构造器:所有需反序列化的 Bean 必须保留公共无参构造器,否则 ParseException。
  • 注解包名:9.2.0 的 @Bean/@Swap 在 org.apache.juneau.annotation;REST 注解在 org.apache.juneau.rest.annotation;请求体用 @Content 而非旧版 @Body。
  • 媒体类型:XML 是 text/xml;application/xml 会 406;curl 不带 Accept 默认返回 HTML 页面。
  • RestClient 需注册解析器:RestClient.create().json().build() 后才能 as() 反序列化。
  • 字符串断言带引号:assertContent 比较原文,JSON 响应中的字符串被引号包裹。
  • 构件版本:9.2.0 没有 juneau-all 与 juneau-marshall-rdf,依赖写错直接解析失败。
  • 运行中 jar 不可被覆盖:服务运行期间重新 mvn package 会触发 NoClassDefFoundError,先停服务再打包。

十一、常见问题(FAQ)

Q1:parse() 报 Class could not be instantiated?

Bean 缺无参构造器,补 public Pet() {}(实测补齐后消失)。

Q2:getContent().as(...) 报 Unsupported media-type?

RestClient 未注册解析器,构造时加 .json()(或对应格式方法)。

Q3:XML 请求 406?

Accept 改为 text/xml(支持清单见 7.4 的 406 回显)。

Q4:返回值 String 在 JSON 响应里带引号,怎么拿纯文本?

Accept: text/plain 走 PlainText 序列化器(对象仍输出 JSON 风格文本,字符串方法返回原文)。

Q5:微服务端口不对?

检查 demo.cfg [Jetty]/port 与 resolveVars = true;端口被占会自动回落(见配置参数详解 5.1)。

Q6:如何看服务端 OpenAPI 文档?

GET /api 返回 OpenAPI 3.0.1 JSON(见 7.3);HTML 页面导航栏亦有 /api、/stats 链接。

十二、参考链接