简介
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 的区别
| 对比项 | Jetty | Tomcat |
|---|---|---|
| 类型 | HTTP 服务器 / Servlet 容器 | Servlet 容器 / Java Web 服务器 |
| 嵌入式体验 | 很强,API 设计偏可编程 | 支持,但日常感知没有 Jetty 强 |
| 独立部署 | 支持 | 支持 |
| WAR 部署 | 支持 | 支持 |
| Spring Boot | 可替换默认 Tomcat | Spring Boot Web 默认容器 |
| Handler API | Jetty 特色能力 | 没有同款核心模型 |
| 适合场景 | 嵌入式、微服务、工具、网关、长连接 | 传统 Web、Spring MVC、企业后台 |
Jetty 里很重要的概念是 Handler。
可以把它理解成:
1请求进来后,Jetty 先交给 Handler 树处理。 2
如果使用 Servlet,Servlet 也是挂在 Jetty 的 Handler 体系下面。
版本怎么选
Jetty 版本和 Java、Servlet / Jakarta EE 规范强相关。
| Jetty 版本线 | Java 要求 | EE 体系 | 包名 |
|---|---|---|---|
| Jetty 9.4.x | Java 8 | Java EE / Servlet 老项目 | javax.servlet.* |
| Jetty 10.0.x | Java 11 | Jakarta EE 8 | javax.servlet.* |
| Jetty 11.0.x | Java 11 | Jakarta EE 9 | jakarta.servlet.* |
| Jetty 12.0.x | Java 17 | Jakarta EE 8 / 9 / 10 | javax.* 和 jakarta.* 按环境区分 |
| Jetty 12.1.x | Java 17 | Jakarta EE 8 / 9 / 10 / 11 | javax.* 和 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
几个核心对象:
| 对象 | 作用 |
|---|---|
| Server | Jetty 服务实例,负责管理生命周期 |
| Connector | 监听端口,接收客户端连接 |
| ServerConnector | 常见服务端连接器 |
| Handler | Jetty 请求处理核心 |
| ContextHandler | 给 Handler 增加上下文路径,比如 /shop |
| ServletContextHandler | 支持 Servlet / Filter,不需要 WAR 包 |
| WebAppContext | 支持标准 WAR 包和 WEB-INF/web.xml |
| QueuedThreadPool | Jetty 工作线程池 |
最小结构:
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 的区别:
| 对比项 | ServletContextHandler | WebAppContext |
|---|---|---|
| 是否需要 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。
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.selectors | I/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 |
| 已经熟悉 Servlet | ServletContextHandler |
| 需要 Filter / Session | ServletContextHandler |
| 标准 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 容器替换》 是转载文章,点击查看原文。