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

简介

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

它主要能做这些事:

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

一句话概括:

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

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

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

Jetty 适合什么场景

常见场景如下:

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

简单理解:

text 复制代码
Tomcat 更常见于传统 Java Web 容器
Jetty 更常见于嵌入式服务、工具服务和高度可编程的 Web 底座

这不是说 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

可以把它理解成:

text 复制代码
请求进来后,Jetty 先交给 Handler 树处理。

如果使用 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.* 按环境区分

新项目常见选择:

text 复制代码
Java 17+
Jetty 12.0.x 稳定线
Jakarta EE 10
jakarta.servlet.*

老项目如果还在使用:

java 复制代码
import javax.servlet.http.HttpServlet;

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

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

java 复制代码
import jakarta.servlet.http.HttpServlet;

Jetty 的两种使用方式

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

独立运行

先下载 Jetty,再部署应用。

大致像这样:

text 复制代码
jetty-home
jetty-base
webapps
start.jar

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

这种方式适合:

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

嵌入式运行

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

大致像这样:

java 复制代码
public static void main(String[] args) {
    Server server = new Server(8080);
    server.setHandler(...);
    server.start();
}

这种方式适合:

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

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

核心架构

Jetty 处理请求的大致流程:

text 复制代码
Client
  |
  v
Connector
  |
  v
Server
  |
  v
Handler
  |
  v
Servlet / Filter / WebSocket / 自定义处理逻辑

几个核心对象:

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

最小结构:

text 复制代码
Server
  |
  v
ServerConnector :8080
  |
  v
Handler

Servlet 结构:

text 复制代码
Server
  |
  v
ServletContextHandler /api
  |
  ├── UserServlet /users/*
  └── LogFilter /*

Maven 依赖

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

版本号可以统一放到 properties:

xml 复制代码
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <jetty.version>12.0.37</jetty.version>
    <jackson.version>2.17.2</jackson.version>
</properties>

最小 Handler 服务只需要 jetty-server

xml 复制代码
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>${jetty.version}</version>
</dependency>

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

xml 复制代码
<dependency>
    <groupId>org.eclipse.jetty.ee10</groupId>
    <artifactId>jetty-ee10-servlet</artifactId>
    <version>${jetty.version}</version>
</dependency>

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

xml 复制代码
<dependency>
    <groupId>org.eclipse.jetty.ee10</groupId>
    <artifactId>jetty-ee10-webapp</artifactId>
    <version>${jetty.version}</version>
</dependency>

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

xml 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${jackson.version}</version>
</dependency>

完整依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-server</artifactId>
        <version>${jetty.version}</version>
    </dependency>

    <dependency>
        <groupId>org.eclipse.jetty.ee10</groupId>
        <artifactId>jetty-ee10-servlet</artifactId>
        <version>${jetty.version}</version>
    </dependency>

    <dependency>
        <groupId>org.eclipse.jetty.ee10</groupId>
        <artifactId>jetty-ee10-webapp</artifactId>
        <version>${jetty.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
</dependencies>

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

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

这个 Demo 只做一件事:

text 复制代码
访问 http://localhost:8080/hello
返回一段 JSON

启动类:

java 复制代码
package com.example.jettydemo;

import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.util.Callback;

public class HelloHandlerServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        ServerConnector connector = new ServerConnector(server);
        connector.setHost("0.0.0.0");
        connector.setPort(8080);
        connector.setIdleTimeout(30_000);
        server.addConnector(connector);

        server.setHandler(new Handler.Abstract() {
            @Override
            public boolean handle(Request request, Response response, Callback callback) {
                String path = request.getHttpURI().getPath();

                if (!"/hello".equals(path)) {
                    response.setStatus(404);
                    response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json;charset=UTF-8");
                    Content.Sink.write(response, true, "{\"message\":\"not found\"}", callback);
                    return true;
                }

                response.setStatus(200);
                response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json;charset=UTF-8");
                Content.Sink.write(response, true, "{\"message\":\"Hello Jetty\"}", callback);
                return true;
            }
        });

        server.start();
        System.out.println("Jetty started: http://localhost:8080/hello");
        server.join();
    }
}

测试:

shell 复制代码
curl http://localhost:8080/hello

返回:

json 复制代码
{"message":"Hello Jetty"}

访问不存在的路径:

shell 复制代码
curl http://localhost:8080/abc

返回:

json 复制代码
{"message":"not found"}

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

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

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

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

Handler 为什么要返回 boolean

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

java 复制代码
return true;

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

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

java 复制代码
Content.Sink.write(response, true, body, callback);

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

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

java 复制代码
callback.succeeded();

或异常时调用:

java 复制代码
callback.failed(error);

否则请求可能一直挂着。

第二个 Demo:Servlet 版 REST 接口

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

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

这个 Demo 实现三个接口:

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

返回对象

java 复制代码
package com.example.jettydemo.dto;

public record ApiResult<T>(int code, String message, T data) {

    public static <T> ApiResult<T> ok(T data) {
        return new ApiResult<>(200, "success", data);
    }

    public static <T> ApiResult<T> fail(int code, String message) {
        return new ApiResult<>(code, message, null);
    }
}

Servlet

java 复制代码
package com.example.jettydemo.servlet;

import com.example.jettydemo.dto.ApiResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class UserServlet extends HttpServlet {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("application/json;charset=UTF-8");

        String id = request.getParameter("id");
        if (id == null || id.isBlank()) {
            ApiResult<List<Map<String, Object>>> result = ApiResult.ok(List.of(
                    Map.of("id", 1, "name", "张三"),
                    Map.of("id", 2, "name", "李四")
            ));
            objectMapper.writeValue(response.getWriter(), result);
            return;
        }

        ApiResult<Map<String, Object>> result = ApiResult.ok(
                Map.of("id", Long.parseLong(id), "name", "用户" + id)
        );
        objectMapper.writeValue(response.getWriter(), result);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("application/json;charset=UTF-8");

        String name = request.getParameter("name");
        if (name == null || name.isBlank()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            objectMapper.writeValue(response.getWriter(), ApiResult.fail(400, "name 不能为空"));
            return;
        }

        ApiResult<Map<String, Object>> result = ApiResult.ok(
                Map.of("id", System.currentTimeMillis(), "name", name)
        );
        objectMapper.writeValue(response.getWriter(), result);
    }
}

启动类

java 复制代码
package com.example.jettydemo;

import com.example.jettydemo.servlet.UserServlet;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;

public class ServletApiServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        connector.setIdleTimeout(30_000);
        server.addConnector(connector);

        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/api");
        context.addServlet(UserServlet.class, "/users");

        server.setHandler(context);
        server.start();

        System.out.println("Jetty Servlet API started: http://localhost:8080/api/users");
        server.join();
    }
}

测试:

shell 复制代码
curl http://localhost:8080/api/users

返回:

json 复制代码
{
  "code": 200,
  "message": "success",
  "data": [
    {
      "id": 1,
      "name": "张三"
    },
    {
      "id": 2,
      "name": "李四"
    }
  ]
}

查询单个用户:

shell 复制代码
curl 'http://localhost:8080/api/users?id=100'

新增用户:

shell 复制代码
curl -X POST 'http://localhost:8080/api/users?name=王五'

加一个 Filter:接口耗时日志

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

可以写一个 Filter:

java 复制代码
package com.example.jettydemo.servlet;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class AccessLogFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        long start = System.currentTimeMillis();

        try {
            chain.doFilter(request, response);
        } finally {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            long cost = System.currentTimeMillis() - start;
            System.out.println(httpRequest.getMethod() + " " + httpRequest.getRequestURI() + " " + cost + "ms");
        }
    }
}

注册 Filter:

java 复制代码
package com.example.jettydemo;

import com.example.jettydemo.servlet.AccessLogFilter;
import com.example.jettydemo.servlet.UserServlet;
import jakarta.servlet.DispatcherType;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;

import java.util.EnumSet;

public class ServletApiServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        server.addConnector(connector);

        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/api");
        context.addServlet(UserServlet.class, "/users");
        context.addFilter(new FilterHolder(new AccessLogFilter()), "/*", EnumSet.of(DispatcherType.REQUEST));

        server.setHandler(context);
        server.start();
        server.join();
    }
}

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

text 复制代码
GET /api/users 12ms

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

第三个 Demo:用 WebAppContext 部署 WAR

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

目录假设如下:

text 复制代码
/opt/apps/user-center.war

启动类:

java 复制代码
package com.example.jettydemo;

import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;

public class WarServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        server.addConnector(connector);

        WebAppContext webAppContext = new WebAppContext();
        webAppContext.setContextPath("/user-center");
        webAppContext.setWar("/opt/apps/user-center.war");

        server.setHandler(webAppContext);
        server.start();

        System.out.println("WAR started: http://localhost:8080/user-center");
        server.join();
    }
}

访问:

text 复制代码
http://localhost:8080/user-center

ServletContextHandlerWebAppContext 的区别:

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

独立 Jetty 部署 WAR

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

常见目录概念:

text 复制代码
JETTY_HOME:Jetty 安装目录
JETTY_BASE:业务配置和应用目录

JETTY_HOME 放 Jetty 自身文件。

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

这种设计方便升级 Jetty:

text 复制代码
升级时替换 JETTY_HOME
业务配置继续留在 JETTY_BASE

创建 base 目录:

shell 复制代码
mkdir jetty-base
cd jetty-base

启用 HTTP 和 EE10 部署模块:

shell 复制代码
java -jar $JETTY_HOME/start.jar --add-modules=http,ee10-deploy

这会创建类似目录:

text 复制代码
jetty-base/
├── resources/
├── start.d/
│   ├── http.ini
│   └── ee10-deploy.ini
└── webapps/

复制 WAR:

shell 复制代码
cp /opt/apps/user-center.war webapps/

启动:

shell 复制代码
java -jar $JETTY_HOME/start.jar

访问:

text 复制代码
http://localhost:8080/user-center

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

比如:

text 复制代码
webapps/user-center.war -> /user-center
webapps/root.war        -> /

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

示例 webapps/user-center.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.ee10.webapp.WebAppContext">
    <Set name="contextPath">/uc</Set>
    <Set name="war">/opt/apps/user-center.war</Set>
</Configure>

这样访问路径就是:

text 复制代码
http://localhost:8080/uc

Jetty Maven 插件

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

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

Jakarta EE 10 项目使用:

xml 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.eclipse.jetty.ee10</groupId>
            <artifactId>jetty-ee10-maven-plugin</artifactId>
            <version>12.0.37</version>
            <configuration>
                <scan>2</scan>
                <webApp>
                    <contextPath>/</contextPath>
                </webApp>
            </configuration>
        </plugin>
    </plugins>
</build>

启动:

shell 复制代码
mvn jetty:run

常见目标:

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

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

xml 复制代码
<scan>2</scan>

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

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

生产环境更适合:

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

Spring Boot 替换 Tomcat 为 Jetty

Spring Boot Web 默认使用 Tomcat。

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

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Controller 不需要改:

java 复制代码
package com.example.bootjetty.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class HealthController {

    @GetMapping("/health")
    public Map<String, Object> health() {
        return Map.of(
                "status", "UP",
                "container", "Jetty"
        );
    }
}

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

访问:

shell 复制代码
curl http://localhost:8080/api/health

返回:

json 复制代码
{
  "status": "UP",
  "container": "Jetty"
}

Spring Boot 里常见 Jetty 配置:

yaml 复制代码
server:
  port: 8080
  jetty:
    connection-idle-timeout: 30s
    max-connections: 10000
    max-http-form-post-size: 2MB
    threads:
      min: 8
      max: 200
      acceptors: -1
      selectors: -1
      idle-timeout: 60s

说明:

配置 作用
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 选择器线程数

acceptorsselectors 通常先保持默认 -1

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

自定义 Spring Boot 内嵌 Jetty

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

比如调整 Connector 的空闲超时:

java 复制代码
package com.example.bootjetty.config;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ServerConnector;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JettyConfig {

    @Bean
    public WebServerFactoryCustomizer<JettyServletWebServerFactory> jettyCustomizer() {
        return factory -> factory.addServerCustomizers(server -> {
            for (Connector connector : server.getConnectors()) {
                if (connector instanceof ServerConnector serverConnector) {
                    serverConnector.setIdleTimeout(30_000);
                }
            }
        });
    }
}

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

线程模型怎么理解

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

可以粗略理解成:

text 复制代码
Acceptor 负责接收连接
Selector 负责监听 I/O 事件
Worker 负责执行请求处理逻辑

QueuedThreadPool 是 Jetty 常见线程池。

简单嵌入式配置示例:

java 复制代码
package com.example.jettydemo;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

public class ThreadPoolServer {

    public static void main(String[] args) throws Exception {
        QueuedThreadPool threadPool = new QueuedThreadPool();
        threadPool.setName("jetty-worker");
        threadPool.setMinThreads(8);
        threadPool.setMaxThreads(200);
        threadPool.setIdleTimeout(60_000);

        Server server = new Server(threadPool);

        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        server.addConnector(connector);

        server.start();
        server.join();
    }
}

线程数不是越大越好。

需要看这些因素:

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

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

更有效的优化往往是:

text 复制代码
缩短接口耗时
控制下游超时
限制队列长度
做好限流
给慢任务单独线程池

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

示例:

java 复制代码
package com.example.jettydemo;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.resource.ResourceFactory;

import java.nio.file.Path;
import java.util.List;

public class StaticFileServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        server.addConnector(connector);

        ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource(Path.of("public")));
        resourceHandler.setDirAllowed(false);
        resourceHandler.setWelcomeFiles(List.of("index.html"));
        resourceHandler.setAcceptRanges(true);

        server.setHandler(resourceHandler);
        server.start();
        server.join();
    }
}

目录:

text 复制代码
project/
├── public/
│   └── index.html
└── src/

访问:

text 复制代码
http://localhost:8080/index.html

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

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

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

text 复制代码
Nginx / CDN 处理静态资源
Jetty 处理动态接口

HTTPS 简单配置

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

生成本地测试证书:

shell 复制代码
keytool -genkeypair \
  -alias jetty-demo \
  -keyalg RSA \
  -keysize 2048 \
  -storetype PKCS12 \
  -keystore jetty-demo.p12 \
  -validity 365

Jetty HTTPS Connector 示例:

java 复制代码
package com.example.jettydemo;

import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;

public class HttpsServer {

    public static void main(String[] args) throws Exception {
        Server server = new Server();

        HttpConfiguration httpsConfig = new HttpConfiguration();
        httpsConfig.addCustomizer(new SecureRequestCustomizer());

        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setKeyStorePath("jetty-demo.p12");
        sslContextFactory.setKeyStorePassword("changeit");

        ServerConnector httpsConnector = new ServerConnector(
                server,
                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
                new HttpConnectionFactory(httpsConfig)
        );
        httpsConnector.setPort(8443);

        server.addConnector(httpsConnector);
        server.start();
        server.join();
    }
}

访问:

text 复制代码
https://localhost:8443

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

text 复制代码
公网 HTTPS 终止在 Nginx / 网关 / SLB
内网 HTTP 转发到 Jetty

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

日志和访问日志

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

yaml 复制代码
server:
  jetty:
    accesslog:
      enabled: true
      filename: logs/jetty-access.log
      retention-period: 14
      append: true

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

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

生产日志至少要包含:

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

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

常见坑

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

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

Jakarta EE 10 应该使用:

java 复制代码
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;

Maven 依赖:

xml 复制代码
<dependency>
    <groupId>org.eclipse.jetty.ee10</groupId>
    <artifactId>jetty-ee10-servlet</artifactId>
    <version>${jetty.version}</version>
</dependency>

老文章里的:

java 复制代码
import org.eclipse.jetty.servlet.ServletContextHandler;

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

javax.servlet 和 jakarta.servlet 混用

常见报错:

text 复制代码
ClassNotFoundException: javax.servlet.http.HttpServlet

或:

text 复制代码
ClassCastException: ... jakarta.servlet.Servlet

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

判断方式:

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

Spring Boot 3 使用 jakarta.servlet.*

Spring Boot 2 使用 javax.servlet.*

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

检查 callback 是否完成。

正确示例:

java 复制代码
Content.Sink.write(response, true, body, callback);
return true;

或者手动:

java 复制代码
callback.succeeded();
return true;

异常时:

java 复制代码
callback.failed(error);
return true;

Spring Boot 已经引入 Jetty 但还是 Tomcat

检查是否排除了 Tomcat:

xml 复制代码
<exclusions>
    <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
</exclusions>

再检查依赖树:

shell 复制代码
mvn dependency:tree

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

Maven 插件启动失败

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

Jakarta EE 10:

xml 复制代码
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-maven-plugin</artifactId>

不是旧写法:

xml 复制代码
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>

端口被占用

报错类似:

text 复制代码
java.net.BindException: Address already in use

说明端口已经被占用。

查看端口:

shell 复制代码
lsof -i :8080

换端口:

java 复制代码
connector.setPort(8081);

或 Spring Boot 配置:

yaml 复制代码
server:
  port: 8081

总结

Jetty 可以按这条线理解:

text 复制代码
Server 管生命周期
Connector 管端口和连接
Handler 管请求处理
ServletContextHandler 管 Servlet / Filter
WebAppContext 管 WAR 包
Spring Boot starter 负责把 Jetty 接到 Boot 启动流程里

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

  • 极简服务: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 用起来就不会乱。

相关推荐
腻害兔9 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
萧瑟余晖11 小时前
JDK 26 新特性详解
java·开发语言
马优晨12 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
维天说15 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane199415 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
噢,我明白了16 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品16 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘16 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts