Java Jetty 实战详解:从嵌入式 HTTP 服务到 Spring Boot 容器替换

作者:唐青枫日期:2026/7/25

简介

Jetty 是 Eclipse 基金会维护的 Java Web 服务器,也是一个 Servlet 容器。

它主要能做这些事:

1监听 HTTP 端口
2处理 HTTP 请求和响应
3运行 Servlet / Filter / Listener
4部署 WAR 包
5支持 WebSocket、HTTP/2、HTTP/3
6作为库嵌入到 Java 程序里启动
7作为 Spring Boot 的内嵌容器
8

一句话概括:

1Jetty 既能像 Tomcat 一样单独部署 Web 应用,也能像普通 Java 库一样嵌入到 main 方法里启动。
2

Jetty 最突出的特点是“嵌入式”和“可编程”。

很多中间件、网关、测试工具、微服务框架喜欢用 Jetty,原因很直接:启动快、模块化、代码里就能组装 HTTP 服务,不一定要准备一个完整应用服务器。

Jetty 适合什么场景

常见场景如下:

场景说明
嵌入式 HTTP 服务普通 Java 程序里直接启动一个 Web 管理端口
REST API不想引入完整 Spring MVC 时,可以直接用 Handler 或 Servlet
Spring Boot 内嵌容器替换默认 Tomcat,用 Jetty 承载 Web 应用
传统 WAR 部署独立 Jetty 服务部署标准 Web 应用
WebSocket 服务聊天、推送、实时状态
测试环境用 Maven 插件快速跑 Web 项目
工具型程序本地工具、桌面程序、采集程序暴露健康检查接口

简单理解:

1Tomcat 更常见于传统 Java Web 容器
2Jetty 更常见于嵌入式服务、工具服务和高度可编程的 Web 底座
3

这不是说 Tomcat 不能嵌入,也不是说 Jetty 不能部署 WAR。

只是两个项目的气质不太一样。

Jetty 和 Tomcat 的区别

对比项JettyTomcat
类型HTTP 服务器 / Servlet 容器Servlet 容器 / Java Web 服务器
嵌入式体验很强,API 设计偏可编程支持,但日常感知没有 Jetty 强
独立部署支持支持
WAR 部署支持支持
Spring Boot可替换默认 TomcatSpring Boot Web 默认容器
Handler APIJetty 特色能力没有同款核心模型
适合场景嵌入式、微服务、工具、网关、长连接传统 Web、Spring MVC、企业后台

Jetty 里很重要的概念是 Handler。

可以把它理解成:

1请求进来后,Jetty 先交给 Handler 树处理。
2

如果使用 Servlet,Servlet 也是挂在 Jetty 的 Handler 体系下面。

版本怎么选

Jetty 版本和 Java、Servlet / Jakarta EE 规范强相关。

Jetty 版本线Java 要求EE 体系包名
Jetty 9.4.xJava 8Java EE / Servlet 老项目javax.servlet.*
Jetty 10.0.xJava 11Jakarta EE 8javax.servlet.*
Jetty 11.0.xJava 11Jakarta EE 9jakarta.servlet.*
Jetty 12.0.xJava 17Jakarta EE 8 / 9 / 10javax.* 和 jakarta.* 按环境区分
Jetty 12.1.xJava 17Jakarta EE 8 / 9 / 10 / 11javax.* 和 jakarta.* 按环境区分

新项目常见选择:

1Java 17+
2Jetty 12.0.x 稳定线
3Jakarta EE 10
4jakarta.servlet.*
5

老项目如果还在使用:

1import javax.servlet.http.HttpServlet;
2

不能直接照搬 Jetty 12 的 EE10 示例。

需要选择对应的 EE8 模块,或者先把代码迁移到:

1import jakarta.servlet.http.HttpServlet;
2

Jetty 的两种使用方式

Jetty 常见用法可以分成两类。

独立运行

先下载 Jetty,再部署应用。

大致像这样:

1jetty-home
2jetty-base
3webapps
4start.jar
5

然后把 WAR 包放进去,由 Jetty 启动和管理。

这种方式适合:

  • 传统 WAR 项目
  • 多个 Web 应用统一部署
  • 运维希望容器和应用分开管理

嵌入式运行

把 Jetty 当成 Maven 依赖引进来,在 main 方法里启动。

大致像这样:

1public static void main(String[] args) {
2    Server server = new Server(8080);
3    server.setHandler(...);
4    server.start();
5}
6

这种方式适合:

  • 微服务
  • 工具程序
  • 单个 Jar 包部署
  • 测试服务
  • 内部管理端口

Spring Boot 内嵌 Jetty 本质上也是嵌入式模式。

核心架构

Jetty 处理请求的大致流程:

1Client
2  |
3  v
4Connector
5  |
6  v
7Server
8  |
9  v
10Handler
11  |
12  v
13Servlet / Filter / WebSocket / 自定义处理逻辑
14

几个核心对象:

对象作用
ServerJetty 服务实例,负责管理生命周期
Connector监听端口,接收客户端连接
ServerConnector常见服务端连接器
HandlerJetty 请求处理核心
ContextHandler给 Handler 增加上下文路径,比如 /shop
ServletContextHandler支持 Servlet / Filter,不需要 WAR 包
WebAppContext支持标准 WAR 包和 WEB-INF/web.xml
QueuedThreadPoolJetty 工作线程池

最小结构:

1Server
2  |
3  v
4ServerConnector :8080
5  |
6  v
7Handler
8

Servlet 结构:

1Server
2  |
3  v
4ServletContextHandler /api
5  |
6  ├── UserServlet /users/*
7  └── LogFilter /*
8

Maven 依赖

下面示例基于 Jetty 12.0.x 和 Jakarta EE 10。

版本号可以统一放到 properties:

1<properties>
2    <maven.compiler.source>17</maven.compiler.source>
3    <maven.compiler.target>17</maven.compiler.target>
4    <jetty.version>12.0.37</jetty.version>
5    <jackson.version>2.17.2</jackson.version>
6</properties>
7

最小 Handler 服务只需要 jetty-server:

1<dependency>
2    <groupId>org.eclipse.jetty</groupId>
3    <artifactId>jetty-server</artifactId>
4    <version>${jetty.version}</version>
5</dependency>
6

如果要写 Servlet,需要引入 EE10 Servlet 模块:

1<dependency>
2    <groupId>org.eclipse.jetty.ee10</groupId>
3    <artifactId>jetty-ee10-servlet</artifactId>
4    <version>${jetty.version}</version>
5</dependency>
6

如果要部署 WAR,需要引入 WebApp 模块:

1<dependency>
2    <groupId>org.eclipse.jetty.ee10</groupId>
3    <artifactId>jetty-ee10-webapp</artifactId>
4    <version>${jetty.version}</version>
5</dependency>
6

Demo 里为了输出 JSON,再加 Jackson:

1<dependency>
2    <groupId>com.fasterxml.jackson.core</groupId>
3    <artifactId>jackson-databind</artifactId>
4    <version>${jackson.version}</version>
5</dependency>
6

完整依赖:

1<dependencies>
2    <dependency>
3        <groupId>org.eclipse.jetty</groupId>
4        <artifactId>jetty-server</artifactId>
5        <version>${jetty.version}</version>
6    </dependency>
7
8    <dependency>
9        <groupId>org.eclipse.jetty.ee10</groupId>
10        <artifactId>jetty-ee10-servlet</artifactId>
11        <version>${jetty.version}</version>
12    </dependency>
13
14    <dependency>
15        <groupId>org.eclipse.jetty.ee10</groupId>
16        <artifactId>jetty-ee10-webapp</artifactId>
17        <version>${jetty.version}</version>
18    </dependency>
19
20    <dependency>
21        <groupId>com.fasterxml.jackson.core</groupId>
22        <artifactId>jackson-databind</artifactId>
23        <version>${jackson.version}</version>
24    </dependency>
25</dependencies>
26

第一个 Demo:不用 Servlet 的 HTTP 服务

Jetty 12 推荐直接使用 Handler.Abstract 写底层 HTTP 处理逻辑。

这个 Demo 只做一件事:

1访问 http://localhost:8080/hello
2返回一段 JSON
3

启动类:

1package com.example.jettydemo;
2
3import org.eclipse.jetty.http.HttpHeader;
4import org.eclipse.jetty.io.Content;
5import org.eclipse.jetty.server.Request;
6import org.eclipse.jetty.server.Response;
7import org.eclipse.jetty.server.Server;
8import org.eclipse.jetty.server.ServerConnector;
9import org.eclipse.jetty.server.Handler;
10import org.eclipse.jetty.util.Callback;
11
12public class HelloHandlerServer {
13
14    public static void main(String[] args) throws Exception {
15        Server server = new Server();
16
17        ServerConnector connector = new ServerConnector(server);
18        connector.setHost("0.0.0.0");
19        connector.setPort(8080);
20        connector.setIdleTimeout(30_000);
21        server.addConnector(connector);
22
23        server.setHandler(new Handler.Abstract() {
24            @Override
25            public boolean handle(Request request, Response response, Callback callback) {
26                String path = request.getHttpURI().getPath();
27
28                if (!"/hello".equals(path)) {
29                    response.setStatus(404);
30                    response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json;charset=UTF-8");
31                    Content.Sink.write(response, true, "{\"message\":\"not found\"}", callback);
32                    return true;
33                }
34
35                response.setStatus(200);
36                response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json;charset=UTF-8");
37                Content.Sink.write(response, true, "{\"message\":\"Hello Jetty\"}", callback);
38                return true;
39            }
40        });
41
42        server.start();
43        System.out.println("Jetty started: http://localhost:8080/hello");
44        server.join();
45    }
46}
47

测试:

1curl http://localhost:8080/hello
2

返回:

1{"message":"Hello Jetty"}
2

访问不存在的路径:

1curl http://localhost:8080/abc
2

返回:

1{"message":"not found"}
2

这个例子没有 Servlet,没有 Spring MVC,也没有注解路由。

请求是直接交给 Handler 处理的。

这种方式适合做非常轻量的接口,比如:

  • 健康检查
  • 本地管理端口
  • 内部指标接口
  • 简单代理服务
  • 不想引入 Web 框架的小工具

Handler 为什么要返回 boolean

handle 方法返回 true 表示请求已经被当前 Handler 处理。

1return true;
2

如果是 Handler 组合结构,返回值会影响后续 Handler 是否继续处理。

同时,响应写完后需要通知 callback。

1Content.Sink.write(response, true, body, callback);
2

这行代码里,Content.Sink.write 会在写入完成后处理 callback。

如果手动处理异步写入,需要确保最后调用:

1callback.succeeded();
2

或异常时调用:

1callback.failed(error);
2

否则请求可能一直挂着。

第二个 Demo:Servlet 版 REST 接口

很多 Java Web 项目还是 Servlet 模型。

Jetty 可以不用 WAR 包,直接用 ServletContextHandler 在代码里注册 Servlet。

这个 Demo 实现三个接口:

方法路径作用
GET/api/users查询用户列表
GET/api/users?id=1查询单个用户
POST/api/users?name=张三新增用户

返回对象

1package com.example.jettydemo.dto;
2
3public record ApiResult<T>(int code, String message, T data) {
4
5    public static <T> ApiResult<T> ok(T data) {
6        return new ApiResult<>(200, "success", data);
7    }
8
9    public static <T> ApiResult<T> fail(int code, String message) {
10        return new ApiResult<>(code, message, null);
11    }
12}
13

Servlet

1package com.example.jettydemo.servlet;
2
3import com.example.jettydemo.dto.ApiResult;
4import com.fasterxml.jackson.databind.ObjectMapper;
5import jakarta.servlet.ServletException;
6import jakarta.servlet.http.HttpServlet;
7import jakarta.servlet.http.HttpServletRequest;
8import jakarta.servlet.http.HttpServletResponse;
9
10import java.io.IOException;
11import java.util.List;
12import java.util.Map;
13
14public class UserServlet extends HttpServlet {
15
16    private final ObjectMapper objectMapper = new ObjectMapper();
17
18    @Override
19    protected void doGet(HttpServletRequest request, HttpServletResponse response)
20            throws ServletException, IOException {
21        response.setContentType("application/json;charset=UTF-8");
22
23        String id = request.getParameter("id");
24        if (id == null || id.isBlank()) {
25            ApiResult<List<Map<String, Object>>> result = ApiResult.ok(List.of(
26                    Map.of("id", 1, "name", "张三"),
27                    Map.of("id", 2, "name", "李四")
28            ));
29            objectMapper.writeValue(response.getWriter(), result);
30            return;
31        }
32
33        ApiResult<Map<String, Object>> result = ApiResult.ok(
34                Map.of("id", Long.parseLong(id), "name", "用户" + id)
35        );
36        objectMapper.writeValue(response.getWriter(), result);
37    }
38
39    @Override
40    protected void doPost(HttpServletRequest request, HttpServletResponse response)
41            throws ServletException, IOException {
42        response.setContentType("application/json;charset=UTF-8");
43
44        String name = request.getParameter("name");
45        if (name == null || name.isBlank()) {
46            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
47            objectMapper.writeValue(response.getWriter(), ApiResult.fail(400, "name 不能为空"));
48            return;
49        }
50
51        ApiResult<Map<String, Object>> result = ApiResult.ok(
52                Map.of("id", System.currentTimeMillis(), "name", name)
53        );
54        objectMapper.writeValue(response.getWriter(), result);
55    }
56}
57

启动类

1package com.example.jettydemo;
2
3import com.example.jettydemo.servlet.UserServlet;
4import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
5import org.eclipse.jetty.server.Server;
6import org.eclipse.jetty.server.ServerConnector;
7
8public class ServletApiServer {
9
10    public static void main(String[] args) throws Exception {
11        Server server = new Server();
12
13        ServerConnector connector = new ServerConnector(server);
14        connector.setPort(8080);
15        connector.setIdleTimeout(30_000);
16        server.addConnector(connector);
17
18        ServletContextHandler context = new ServletContextHandler();
19        context.setContextPath("/api");
20        context.addServlet(UserServlet.class, "/users");
21
22        server.setHandler(context);
23        server.start();
24
25        System.out.println("Jetty Servlet API started: http://localhost:8080/api/users");
26        server.join();
27    }
28}
29

测试:

1curl http://localhost:8080/api/users
2

返回:

1{
2  "code": 200,
3  "message": "success",
4  "data": [
5    {
6      "id": 1,
7      "name": "张三"
8    },
9    {
10      "id": 2,
11      "name": "李四"
12    }
13  ]
14}
15

查询单个用户:

1curl 'http://localhost:8080/api/users?id=100'
2

新增用户:

1curl -X POST 'http://localhost:8080/api/users?name=王五'
2

加一个 Filter:接口耗时日志

Servlet 项目常见需求是记录请求日志。

可以写一个 Filter:

1package com.example.jettydemo.servlet;
2
3import jakarta.servlet.Filter;
4import jakarta.servlet.FilterChain;
5import jakarta.servlet.ServletException;
6import jakarta.servlet.ServletRequest;
7import jakarta.servlet.ServletResponse;
8import jakarta.servlet.http.HttpServletRequest;
9
10import java.io.IOException;
11
12public class AccessLogFilter implements Filter {
13
14    @Override
15    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
16            throws IOException, ServletException {
17        long start = System.currentTimeMillis();
18
19        try {
20            chain.doFilter(request, response);
21        } finally {
22            HttpServletRequest httpRequest = (HttpServletRequest) request;
23            long cost = System.currentTimeMillis() - start;
24            System.out.println(httpRequest.getMethod() + " " + httpRequest.getRequestURI() + " " + cost + "ms");
25        }
26    }
27}
28

注册 Filter:

1package com.example.jettydemo;
2
3import com.example.jettydemo.servlet.AccessLogFilter;
4import com.example.jettydemo.servlet.UserServlet;
5import jakarta.servlet.DispatcherType;
6import org.eclipse.jetty.ee10.servlet.FilterHolder;
7import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
8import org.eclipse.jetty.server.Server;
9import org.eclipse.jetty.server.ServerConnector;
10
11import java.util.EnumSet;
12
13public class ServletApiServer {
14
15    public static void main(String[] args) throws Exception {
16        Server server = new Server();
17
18        ServerConnector connector = new ServerConnector(server);
19        connector.setPort(8080);
20        server.addConnector(connector);
21
22        ServletContextHandler context = new ServletContextHandler();
23        context.setContextPath("/api");
24        context.addServlet(UserServlet.class, "/users");
25        context.addFilter(new FilterHolder(new AccessLogFilter()), "/*", EnumSet.of(DispatcherType.REQUEST));
26
27        server.setHandler(context);
28        server.start();
29        server.join();
30    }
31}
32

访问接口后,控制台会打印:

1GET /api/users 12ms
2

这一套写法和 web.xml 里配置 Servlet / Filter 的效果类似,只是改成了 Java 代码注册。

第三个 Demo:用 WebAppContext 部署 WAR

如果项目已经是标准 WAR 包,可以用 WebAppContext。

目录假设如下:

1/opt/apps/user-center.war
2

启动类:

1package com.example.jettydemo;
2
3import org.eclipse.jetty.ee10.webapp.WebAppContext;
4import org.eclipse.jetty.server.Server;
5import org.eclipse.jetty.server.ServerConnector;
6
7public class WarServer {
8
9    public static void main(String[] args) throws Exception {
10        Server server = new Server();
11
12        ServerConnector connector = new ServerConnector(server);
13        connector.setPort(8080);
14        server.addConnector(connector);
15
16        WebAppContext webAppContext = new WebAppContext();
17        webAppContext.setContextPath("/user-center");
18        webAppContext.setWar("/opt/apps/user-center.war");
19
20        server.setHandler(webAppContext);
21        server.start();
22
23        System.out.println("WAR started: http://localhost:8080/user-center");
24        server.join();
25    }
26}
27

访问:

1http://localhost:8080/user-center
2

ServletContextHandler 和 WebAppContext 的区别:

对比项ServletContextHandlerWebAppContext
是否需要 WAR不需要通常需要
是否读取 web.xml不读取会读取
组件注册方式Java 代码注册按 WAR / web.xml / 注解
适合场景嵌入式服务、小应用标准 Web 应用、旧项目迁移

独立 Jetty 部署 WAR

Jetty 也可以像传统应用服务器一样独立运行。

常见目录概念:

1JETTY_HOME:Jetty 安装目录
2JETTY_BASE:业务配置和应用目录
3

JETTY_HOME 放 Jetty 自身文件。

JETTY_BASE 放项目配置、模块启用文件、webapps、日志等。

这种设计方便升级 Jetty:

1升级时替换 JETTY_HOME
2业务配置继续留在 JETTY_BASE
3

创建 base 目录:

1mkdir jetty-base
2cd jetty-base
3

启用 HTTP 和 EE10 部署模块:

1java -jar $JETTY_HOME/start.jar --add-modules=http,ee10-deploy
2

这会创建类似目录:

1jetty-base/
2├── resources/
3├── start.d/
4│   ├── http.ini
5│   └── ee10-deploy.ini
6└── webapps/
7

复制 WAR:

1cp /opt/apps/user-center.war webapps/
2

启动:

1java -jar $JETTY_HOME/start.jar
2

访问:

1http://localhost:8080/user-center
2

WAR 文件名通常会影响访问路径。

比如:

1webapps/user-center.war -> /user-center
2webapps/root.war        -> /
3

如果需要自定义 context path,可以使用 Jetty context XML。

示例 webapps/user-center.xml:

1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
3<Configure class="org.eclipse.jetty.ee10.webapp.WebAppContext">
4    <Set name="contextPath">/uc</Set>
5    <Set name="war">/opt/apps/user-center.war</Set>
6</Configure>
7

这样访问路径就是:

1http://localhost:8080/uc
2

Jetty Maven 插件

开发传统 Web 项目时,可以用 Jetty Maven 插件快速启动。

Jetty 12 以后,Maven 插件也按 EE 版本拆包。

Jakarta EE 10 项目使用:

1<build>
2    <plugins>
3        <plugin>
4            <groupId>org.eclipse.jetty.ee10</groupId>
5            <artifactId>jetty-ee10-maven-plugin</artifactId>
6            <version>12.0.37</version>
7            <configuration>
8                <scan>2</scan>
9                <webApp>
10                    <contextPath>/</contextPath>
11                </webApp>
12            </configuration>
13        </plugin>
14    </plugins>
15</build>
16

启动:

1mvn jetty:run
2

常见目标:

目标说明
jetty:run直接运行未打包 Web 应用,开发最常用
jetty:run-war运行已经打好的 WAR
jetty:start启动后不阻塞 Maven,适合绑定构建生命周期
jetty:start-war以 WAR 方式启动后不阻塞
jetty:stop停止

scan 表示扫描变更的间隔秒数。

1<scan>2</scan>
2

表示每 2 秒检查一次变更,发现变化后重新部署。

Jetty Maven 插件适合开发和测试,不适合生产部署。

生产环境更适合:

  • 独立 Jetty
  • 嵌入式 Jetty
  • Spring Boot 可执行 Jar
  • 容器镜像

Spring Boot 替换 Tomcat 为 Jetty

Spring Boot Web 默认使用 Tomcat。

替换成 Jetty,只需要排除 Tomcat,再引入 Jetty starter。

1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-web</artifactId>
4    <exclusions>
5        <exclusion>
6            <groupId>org.springframework.boot</groupId>
7            <artifactId>spring-boot-starter-tomcat</artifactId>
8        </exclusion>
9    </exclusions>
10</dependency>
11
12<dependency>
13    <groupId>org.springframework.boot</groupId>
14    <artifactId>spring-boot-starter-jetty</artifactId>
15</dependency>
16

Controller 不需要改:

1package com.example.bootjetty.controller;
2
3import org.springframework.web.bind.annotation.GetMapping;
4import org.springframework.web.bind.annotation.RequestMapping;
5import org.springframework.web.bind.annotation.RestController;
6
7import java.util.Map;
8
9@RestController
10@RequestMapping("/api")
11public class HealthController {
12
13    @GetMapping("/health")
14    public Map<String, Object> health() {
15        return Map.of(
16                "status", "UP",
17                "container", "Jetty"
18        );
19    }
20}
21

启动后,控制台里会看到 Jetty 相关日志。

访问:

1curl http://localhost:8080/api/health
2

返回:

1{
2  "status": "UP",
3  "container": "Jetty"
4}
5

Spring Boot 里常见 Jetty 配置:

1server:
2  port: 8080
3  jetty:
4    connection-idle-timeout: 30s
5    max-connections: 10000
6    max-http-form-post-size: 2MB
7    threads:
8      min: 8
9      max: 200
10      acceptors: -1
11      selectors: -1
12      idle-timeout: 60s
13

说明:

配置作用
server.jetty.connection-idle-timeout连接空闲多久关闭
server.jetty.max-connections最大连接数
server.jetty.max-http-form-post-size表单 POST 最大大小
server.jetty.threads.min最小线程数
server.jetty.threads.max最大线程数
server.jetty.threads.acceptors接收连接线程数
server.jetty.threads.selectorsI/O 选择器线程数

acceptors 和 selectors 通常先保持默认 -1。

默认值会按运行环境计算。除非已经有压测数据,否则不建议一上来手动调很大。

自定义 Spring Boot 内嵌 Jetty

如果配置文件不够,可以写 WebServerFactoryCustomizer。

比如调整 Connector 的空闲超时:

1package com.example.bootjetty.config;
2
3import org.eclipse.jetty.server.Connector;
4import org.eclipse.jetty.server.ServerConnector;
5import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
6import org.springframework.boot.web.server.WebServerFactoryCustomizer;
7import org.springframework.context.annotation.Bean;
8import org.springframework.context.annotation.Configuration;
9
10@Configuration
11public class JettyConfig {
12
13    @Bean
14    public WebServerFactoryCustomizer<JettyServletWebServerFactory> jettyCustomizer() {
15        return factory -> factory.addServerCustomizers(server -> {
16            for (Connector connector : server.getConnectors()) {
17                if (connector instanceof ServerConnector serverConnector) {
18                    serverConnector.setIdleTimeout(30_000);
19                }
20            }
21        });
22    }
23}
24

这种方式适合配置文件覆盖不到的 Jetty 原生能力。

线程模型怎么理解

Jetty 不是“一个请求一个新线程”这么简单。

可以粗略理解成:

1Acceptor 负责接收连接
2Selector 负责监听 I/O 事件
3Worker 负责执行请求处理逻辑
4

QueuedThreadPool 是 Jetty 常见线程池。

简单嵌入式配置示例:

1package com.example.jettydemo;
2
3import org.eclipse.jetty.server.Server;
4import org.eclipse.jetty.server.ServerConnector;
5import org.eclipse.jetty.util.thread.QueuedThreadPool;
6
7public class ThreadPoolServer {
8
9    public static void main(String[] args) throws Exception {
10        QueuedThreadPool threadPool = new QueuedThreadPool();
11        threadPool.setName("jetty-worker");
12        threadPool.setMinThreads(8);
13        threadPool.setMaxThreads(200);
14        threadPool.setIdleTimeout(60_000);
15
16        Server server = new Server(threadPool);
17
18        ServerConnector connector = new ServerConnector(server);
19        connector.setPort(8080);
20        server.addConnector(connector);
21
22        server.start();
23        server.join();
24    }
25}
26

线程数不是越大越好。

需要看这些因素:

  • CPU 核数
  • 接口是否阻塞
  • 数据库连接池大小
  • 下游接口耗时
  • 请求峰值
  • JVM 内存

如果接口里有大量慢 SQL、远程 HTTP 调用、文件 I/O,Jetty 线程再多也只是把阻塞堆起来。

更有效的优化往往是:

1缩短接口耗时
2控制下游超时
3限制队列长度
4做好限流
5给慢任务单独线程池
6

Handler 和 Servlet 怎么选

需求建议
极简 HTTP 接口Handler
健康检查 / 指标暴露Handler
已经熟悉 ServletServletContextHandler
需要 Filter / SessionServletContextHandler
标准 WAR 包WebAppContext
Spring MVC / Spring Boot交给 Spring Boot Jetty starter

Handler 更轻,但更底层。

Servlet 更熟悉,生态也更完整。

如果只是写业务 REST API,Spring Boot + Jetty 更省心。

如果是做网关、中间件、内部控制面,直接嵌入 Jetty Handler 会更轻。

静态资源

Jetty 可以处理静态资源。

纯静态文件服务可以直接用 ResourceHandler。

示例:

1package com.example.jettydemo;
2
3import org.eclipse.jetty.server.Server;
4import org.eclipse.jetty.server.ServerConnector;
5import org.eclipse.jetty.server.handler.ResourceHandler;
6import org.eclipse.jetty.util.resource.ResourceFactory;
7
8import java.nio.file.Path;
9import java.util.List;
10
11public class StaticFileServer {
12
13    public static void main(String[] args) throws Exception {
14        Server server = new Server();
15
16        ServerConnector connector = new ServerConnector(server);
17        connector.setPort(8080);
18        server.addConnector(connector);
19
20        ResourceHandler resourceHandler = new ResourceHandler();
21        resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource(Path.of("public")));
22        resourceHandler.setDirAllowed(false);
23        resourceHandler.setWelcomeFiles(List.of("index.html"));
24        resourceHandler.setAcceptRanges(true);
25
26        server.setHandler(resourceHandler);
27        server.start();
28        server.join();
29    }
30}
31

目录:

1project/
2├── public/
3│   └── index.html
4└── src/
5

访问:

1http://localhost:8080/index.html
2

如果是 Servlet Web 应用,也可以使用 DefaultServlet 或 ResourceServlet,它们更适合挂在 ServletContextHandler 里。

生产环境里,静态资源通常还是交给 Nginx、CDN 或对象存储。

Jetty 处理静态资源没问题,但业务系统里更常见的结构是:

1Nginx / CDN 处理静态资源
2Jetty 处理动态接口
3

HTTPS 简单配置

嵌入式 Jetty 配置 HTTPS 时,需要准备 keystore。

生成本地测试证书:

1keytool -genkeypair \
2  -alias jetty-demo \
3  -keyalg RSA \
4  -keysize 2048 \
5  -storetype PKCS12 \
6  -keystore jetty-demo.p12 \
7  -validity 365
8

Jetty HTTPS Connector 示例:

1package com.example.jettydemo;
2
3import org.eclipse.jetty.http.HttpVersion;
4import org.eclipse.jetty.server.HttpConfiguration;
5import org.eclipse.jetty.server.HttpConnectionFactory;
6import org.eclipse.jetty.server.SecureRequestCustomizer;
7import org.eclipse.jetty.server.Server;
8import org.eclipse.jetty.server.ServerConnector;
9import org.eclipse.jetty.server.SslConnectionFactory;
10import org.eclipse.jetty.util.ssl.SslContextFactory;
11
12public class HttpsServer {
13
14    public static void main(String[] args) throws Exception {
15        Server server = new Server();
16
17        HttpConfiguration httpsConfig = new HttpConfiguration();
18        httpsConfig.addCustomizer(new SecureRequestCustomizer());
19
20        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
21        sslContextFactory.setKeyStorePath("jetty-demo.p12");
22        sslContextFactory.setKeyStorePassword("changeit");
23
24        ServerConnector httpsConnector = new ServerConnector(
25                server,
26                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
27                new HttpConnectionFactory(httpsConfig)
28        );
29        httpsConnector.setPort(8443);
30
31        server.addConnector(httpsConnector);
32        server.start();
33        server.join();
34    }
35}
36

访问:

1https://localhost:8443
2

真实生产环境里,HTTPS 常见做法是:

1公网 HTTPS 终止在 Nginx / 网关 / SLB
2内网 HTTP 转发到 Jetty
3

如果 Jetty 直接暴露公网 HTTPS,需要认真配置证书、协议版本、加密套件和证书续期流程。

日志和访问日志

Spring Boot 里可以直接开 Jetty access log:

1server:
2  jetty:
3    accesslog:
4      enabled: true
5      filename: logs/jetty-access.log
6      retention-period: 14
7      append: true
8

普通嵌入式 Jetty 可以通过 Handler / Filter 自己记录访问日志。

如果是 Servlet 应用,用前面的 AccessLogFilter 就够处理简单场景。

生产日志至少要包含:

  • 请求方法
  • 请求路径
  • 状态码
  • 耗时
  • 客户端 IP
  • User-Agent
  • TraceId

不要在访问日志里直接打印密码、Token、身份证号、手机号等敏感信息。

常见坑

Jetty 12 里找不到 org.eclipse.jetty.servlet.ServletContextHandler

Jetty 12 的 Servlet 模块按 EE 环境拆开了。

Jakarta EE 10 应该使用:

1import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
2

Maven 依赖:

1<dependency>
2    <groupId>org.eclipse.jetty.ee10</groupId>
3    <artifactId>jetty-ee10-servlet</artifactId>
4    <version>${jetty.version}</version>
5</dependency>
6

老文章里的:

1import org.eclipse.jetty.servlet.ServletContextHandler;
2

多半是 Jetty 9 / 10 / 11 时代的写法。

javax.servlet 和 jakarta.servlet 混用

常见报错:

1ClassNotFoundException: javax.servlet.http.HttpServlet
2

或:

1ClassCastException: ... jakarta.servlet.Servlet
2

原因通常是依赖和代码包名不匹配。

判断方式:

1代码 import javax.servlet.* -> 使用 EE8 / 老版本相关模块
2代码 import jakarta.servlet.* -> 使用 EE9 / EE10 / EE11 相关模块
3

Spring Boot 3 使用 jakarta.servlet.*。

Spring Boot 2 使用 javax.servlet.*。

Handler 写了响应但请求一直不结束

检查 callback 是否完成。

正确示例:

1Content.Sink.write(response, true, body, callback);
2return true;
3

或者手动:

1callback.succeeded();
2return true;
3

异常时:

1callback.failed(error);
2return true;
3

Spring Boot 已经引入 Jetty 但还是 Tomcat

检查是否排除了 Tomcat:

1<exclusions>
2    <exclusion>
3        <groupId>org.springframework.boot</groupId>
4        <artifactId>spring-boot-starter-tomcat</artifactId>
5    </exclusion>
6</exclusions>
7

再检查依赖树:

1mvn dependency:tree
2

如果还能看到 spring-boot-starter-tomcat,说明某个 starter 又间接带了 Tomcat。

Maven 插件启动失败

Jetty 12 插件要选 EE 对应版本。

Jakarta EE 10:

1<groupId>org.eclipse.jetty.ee10</groupId>
2<artifactId>jetty-ee10-maven-plugin</artifactId>
3

不是旧写法:

1<groupId>org.eclipse.jetty</groupId>
2<artifactId>jetty-maven-plugin</artifactId>
3

端口被占用

报错类似:

1java.net.BindException: Address already in use
2

说明端口已经被占用。

查看端口:

1lsof -i :8080
2

换端口:

1connector.setPort(8081);
2

或 Spring Boot 配置:

1server:
2  port: 8081
3

总结

Jetty 可以按这条线理解:

1Server 管生命周期
2Connector 管端口和连接
3Handler 管请求处理
4ServletContextHandler 管 Servlet / Filter
5WebAppContext 管 WAR 包
6Spring Boot starter 负责把 Jetty 接到 Boot 启动流程里
7

日常使用可以按场景选择:

  • 极简服务:Server + Handler
  • Servlet 接口:Server + ServletContextHandler
  • WAR 项目:WebAppContext 或独立 Jetty
  • Spring Boot 项目:排除 Tomcat,引入 spring-boot-starter-jetty
  • 开发调试:jetty-ee10-maven-plugin

Jetty 的优势不在于“概念多”,而在于组合灵活。简单接口可以很轻,标准 Web 应用也能部署,Spring Boot 里还能直接替换容器。把版本、EE 环境、Handler 和 Servlet 的边界理清楚,Jetty 用起来就不会乱。


《Java Jetty 实战详解:从嵌入式 HTTP 服务到 Spring Boot 容器替换》 是转载文章,点击查看原文。


相关推荐


学习 OpenMontage 的工具发现与打分选择器
日习一技2026/7/17

在前面的几篇文章里,我们已经体验了 OpenMontage 的零成本玩法:用 Piper 配音、用免费素材剪纪录片、用 Remotion 把图片做成动画,全程不花一分钱 API 费用。我们也试过贴一个参考视频,让 agent 帮我们拆解出差异化的制作方案。 不过零成本路径终究有上限。想要 FLUX 的图、Veo 或 Kling 的真实运动镜头、ElevenLabs 的高质量配音,就得接入对应的 provider。OpenMontage 支持的 provider 有几十个,同一个能力往往有好几个、


LeetCode 28. 找出字符串中第一个匹配项的下标
Best_Jerry2026/7/9

leetcode.cn/problems/fi… programmercarl.com/0028.%E5%AE… 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 ****。   示例 1: 输入: haystack = "sadbutsad", needle = "sad" 输出: 0 解释: "sad" 在下标 0 和


图解 MongoDB 22|读写关注:持久性与一致性的档位选择
十三Tech2026/7/1

前面几篇多次提到 w: "majority",这篇把它彻底讲清楚。读写关注(read/write concern)是 MongoDB 控制持久性和一致性的核心参数——它们决定了「一个写入要被几个节点确认才算成功」「一个读取从哪个节点读、读到什么程度的一致」。理解了它们,才能在不同业务场景下精准调出「够用且不浪费」的持久性/一致性档位。 先把机制边界说清楚 读写关注是三个相关但独立的参数: writeConcern(写关注):写操作要被几个节点确认才算成功。控制持久性。 readPreferen


图解 MongoDB 05|文档模型设计:内嵌 vs 引用,反范式不是免费午餐
十三Tech2026/6/22

刚从 MySQL 迁到 MongoDB 的人,最容易把关系建模那一套照搬过来:每个实体建一个集合,用 userId、orderId 这种字段做关联,查询时再 $lookup 拼。这种写法能跑,但它把 MongoDB 用成了「没有外键约束的关系库」,丢掉了文档模型最大的优势——访问局部性。 文档模型真正的价值,不是「字段随便加」,而是把一个业务实体的相关信息内嵌成一个文档,应用读一次就能拿到全部信息。但内嵌也不是免费午餐:它换来访问效率的同时,要承担冗余、一致性维护和文档膨胀的成本。这一篇讲清楚内


从 WWDC 26 空间重构(Spatial Reframing)再看端侧 2D 转 3D 的技术演进
Layer2026/6/14

2026 年 6 月 8 日,WWDC26 上苹果发布了空间重构(Spatial Reframing):照片拍完之后,拖动画面重新选择机位,AI 实时补全新视角缺失的内容: 头部玩家在两年内相继入场,这背后是三项能力趋于成熟: 单目深度估计沉淀为基础模型:无需双摄或激光雷达,仅凭一张普通照片推断每个像素的远近;过去这类模型更像“专用工具”,换个场景就容易失准;2024 年前后,香港大学与字节跳动的 Depth Anything V2 的 25M 参数的 Small


Python 迭代器与生成器
copyer_xyf2026/6/7

本文面向已有前端开发基础、正在学习 Python 的开发者。 迭代器和生成器解决的是同一个问题:数据不一定要一次性全部准备好,可以在需要的时候一个一个取出来。前端里最接近的经验是 for...of、Symbol.iterator、生成器函数 function* 和 yield。 这几个概念可以先合在一起记: 可迭代对象表示“可以被遍历的数据源” 迭代器表示“真正负责一步一步取值的对象” 生成器表示“用 yield 快速创建出来的迭代器” 后面的 for 循环,本质上就是先从可迭代对象拿到迭代器


【架构实战】ElasticSearch搜索集群:全文检索的艺术
heimeiyingwang2026/5/31

【架构实战】ElasticSearchæœç´¢é›†ç¾¤ï¼šå ¨æ–‡æ£€ç´¢çš„è‰ºæœ¯ 倒排索引、分片副本、搜索优化、实战案例 ä¸€ã€ä»Žä¸€ä¸ªçœŸå®žçš„æ• äº‹è¯´èµ· 2024年双十一,某电商平台搜索系统在流量洪峰到来的那一刻,突


豆包收费了:3.45亿用户,一个“豆包型人格“的道歉经济学
倔强的石头_2026/5/9

5月4号,两个微博热搜几乎同时炸了——#豆包错误率# 和 #豆包笨还收费#。 前一天,豆包刚刚在App Store页面更新了付费订阅声明:标准版68元/月,加强版200元/月,专业版500元/月。作为目前国内月活超过3.45亿的AI助手——这个数字意味着大约每四个中国人里就有一个人在用豆包——这是字节跳动第一次正式给豆包贴上价格标签。 但市场的反应不是期待,而是愤怒。 “又笨又收费,说平时用免费版,经常答非所问,信息出错,逻辑也不严谨,有时候还一本正经地胡说八道,基础功能都没做好。” “免费的


解锁AI编程密码:程序员常用的10个AI提示词
小码哥_常2026/4/30

解锁AI编程密码:程序员常用的10个AI提示词 引言:AI 时代的编程利器 在当今数字化浪潮中,编程领域正经历着前所未有的变革,AI 的加入让程序员们如虎添翼。有这样一个真实的故事,程序员小李在开发一个电商项目的订单管理模块时,遇到了性能瓶颈。原本处理大量订单数据时需要耗费很长时间,导致用户在下单和查询订单状态时响应迟缓。小李尝试了各种常规优化手段,但效果甚微,他陷入了困境,项目进度也因此受阻。 后来,小李了解到可以借助 AI 来解决问题。他在 AI 编程助手的输入框中输入了这样一个提示词:“A


GPT-Image-2 真有点夯:中文不乱码了!GPT-Image-2的入口在哪?教你如何确认自己是否被灰度推送了 GPT-Image-2
摆烂工程师2026/4/21

不知道大家有没有被 OpenAI 最近推出的 GPT-Image-2 惊讶到! 这几天,我用 GPT-Image-2 制作了各种主题的图片,简直夯爆了! 首先汉字提升巨大!另外就是高密度的文字生成,几乎没有乱码! 测试出来的效果,大家直接去生成对比,目前 GPT-Image-2 就是文生图的新王。 比 Nano Banana Pro 香多了! 怎么体验 GPT-Image-2 呢? 目前,官方已经进行灰度分发到 ChatGPT 上,优先美区,只要订阅了 Plus、Pro、Business 等用户

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读