Jetty 以輕量、嵌入式著稱,是 Spring Boot 內置容器、微服務開發首選。本文系統講解 Jetty 在開發階段熱部署、測試階段自動化啟停、生產階段可執行 WAR 打包三大場景的完整配置與代碼實現,基於 Jetty 9.x / 10.x / 11.x 兼容寫法。
<properties>
<jetty.version>11.0.15</jetty.version>
</properties>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<scanIntervalSeconds>3</scanIntervalSeconds> <!-- 3 秒掃描一次 -->
<webApp>
<contextPath>/${project.artifactId}</contextPath>
</webApp>
<httpConnector>
<port>8080</port>
</httpConnector>
</configuration>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
<version>${jetty.version}</version>
</dependency>
</dependencies>
</plugin>
啟動:mvn jetty:run → 訪問 https://localhost:8080/artifactId。修改 Java/資源文件自動重載,無需重啟。
public class JettyFactory {
public static Server createServer(int port, String contextPath) {
Server server = new Server();
server.setStopAtShutdown(true); // JVM 關閉鉤子
ServerConnector connector = new ServerConnector(server);
connector.setPort(port);
connector.setReuseAddress(false); // Windows 避免端口佔用不報錯
server.setConnectors(new Connector[]{connector});
WebAppContext webApp = new WebAppContext("src/main/webapp", contextPath);
// 解決 Windows 靜態文件鎖定
webApp.setDefaultsDescriptor("jetty/webdefault-windows.xml");
server.setHandler(webApp);
return server;
}
// 設置含 TLD 的 JAR 包掃描模式 (解決 JSTL/Shiro/Sitemesh 標簽找不到)
public static void setTldJarNames(Server server, String... jarNames) {
WebAppContext ctx = (WebAppContext) server.getHandler();
List patterns = new ArrayList<>();
patterns.add(".*/jstl-[^/]*\\.jar$");
patterns.add(".*/.*taglibs[^/]*\\.jar$");
for (String name : jarNames) patterns.add(".*/" + name + "-[^/]*\\.jar$");
ctx.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
String.join("|", patterns));
}
// 快速重載 (開發時手動觸發)
public static void reloadContext(Server server) throws Exception {
WebAppContext ctx = (WebAppContext) server.getHandler();
ctx.stop();
WebAppClassLoader cl = new WebAppClassLoader(ctx);
cl.addClassPath("target/classes");
cl.addClassPath("target/test-classes");
ctx.setClassLoader(cl);
ctx.start();
}
}
啟動類:
public class QuickStartServer {
public static void main(String[] args) throws Exception {
System.setProperty("spring.profiles.active", "development");
Server server = JettyFactory.createServer(8080, "/quickstart");
JettyFactory.setTldJarNames(server, "sitemesh", "spring-webmvc", "shiro-web");
server.start();
System.out.println("Server running at https://localhost:8080/quickstart");
System.out.println("Hit Enter to reload...");
new BufferedReader(new InputStreamReader(System.in)).readLine();
JettyFactory.reloadContext(server);
}
}
javax.servlet.HttpConstraintElement signer mismatch:Jetty 8/9 引入 javax.servlet-api 與 jetty-servlet 簽名衝突 → 排除 servlet-api 依賴 或升級 Jetty 10+ (Jakarta EE 9 命名空間)。<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds> <!-- 測試不熱部署 -->
<stopKey>stopKey</stopKey>
<stopPort>9999</stopPort>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals><goal>start</goal></goals>
<configuration>
<daemon>true</daemon> <!-- 守護進程,隨 Maven 退出 -->
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals><goal>stop</goal></goals>
</execution>
</executions>
</plugin>
流程:mvn verify → pre-integration-test 啟動 Jetty → 運行 IT 測試 → post-integration-test 停止 Jetty。daemon=true 確保測試失敗也能清理進程。
目標:java -jar app.war 直接啟動,內嵌 Jetty 所有類,無需外部容器。
<profile>
<id>standalone</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>single</goal></goals>
<configuration>
<descriptors><descriptor>assembly-standalone.xml</descriptor></descriptors>
<archive>
<manifest><mainClass>com.example.Main</mainClass></manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
assembly-standalone.xml 關鍵點:
<format>war</format>、<includeBaseDirectory>false</includeBaseDirectory>dependencySet:include>org.eclipse.jetty:*、unpack>true、excludes>META-INF/*, about_files/* (避免簽名衝突)。fileSet:打包 target/classes、target/${finalName} (webapp 靜態資源)、Main.class 到根目錄。<profile>
<id>standalone</id>
<build>
<plugins>
<!-- 1. war-plugin 設置 Main-Class -->
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive><manifest><mainClass>com.example.Main</mainClass></manifest></archive>
<warName>${project.artifactId}-standalone</warName>
</configuration>
</plugin>
<!-- 2. antrun 移動 Main.class 到 WAR 根目錄 -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>prepare-package</phase>
<goals><goal>run</goal></goals>
<configuration>
<target>
<move todir="${project.build.directory}/${project.artifactId}/">
<fileset dir="${project.build.directory}/classes/">
<include name="**/*/Main.class"/>
</fileset>
</move>
</target>
</configuration>
</execution>
</executions>
</plugin>
<!-- 3. dependency-plugin 解包 Jetty JAR 到 WAR 根目錄 -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-jetty</id>
<phase>prepare-package</phase>
<goals><goal>unpack-dependencies</goal></goals>
<configuration>
<includeGroupIds>org.eclipse.jetty,org.eclipse.jetty.orbit,javax.servlet</includeGroupIds>
<includeScope>provided</includeScope>
<excludes>META-INF/*,about_files/*</excludes>
<outputDirectory>${project.build.directory}/${project.artifactId}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
public class Main {
public static void main(String[] args) throws Exception {
String contextPath = "/";
int port = Integer.getInteger("port", 8080);
Server server = new Server(port);
server.setStopAtShutdown(true);
ProtectionDomain domain = Main.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
// 路徑含中文需解碼
String warPath = URLDecoder.decode(location.toExternalForm(), "UTF-8");
WebAppContext context = new WebAppContext(warPath, contextPath);
context.setServer(server);
// 工作目錄 (JSP 編譯、解壓 WAR)
File workDir = new File(new File(location.getPath()).getParent(), "work");
context.setTempDirectory(workDir);
server.setHandler(context);
server.start();
server.join(); // 阻塞主線程
}
}
<scope>provided</scope> (不進 WEB-INF/lib,由打包插件解包到根目錄)。org.mortbay.jetty:jsp-2.1-glassfish (Jetty 9) 或 org.eclipse.jetty:jetty-jsp (Jetty 10/11),scope 必須 compile/runtime,否則 JSP 編譯找不到類。mvn clean package -Pstandalone java -Xms2g -Xmx2g -XX:MaxMetaspaceSize=256m -jar target/app-standalone.war
| 項目 | Jetty 9 (javax.*) | Jetty 10/11 (jakarta.*) |
|---|---|---|
| Servlet API | javax.servlet.* | jakarta.servlet.* |
| JSP API | javax.servlet.jsp.* | jakarta.servlet.jsp.* |
| Maven 坐標 | org.eclipse.jetty:jetty-* | org.eclipse.jetty:jetty-* (同坐標,大版本號) |
| web.xml | version="3.1" xmlns="java.sun.com" | version="5.0" xmlns="jakarta.ee" |
代碼遷移可用 OpenRewrite 或 IDE 批量替換包名,注意保持依賴版本一致性。
Jetty 嵌入式三模式覆蓋全生命周期:
jetty-maven-plugin 熱部署 + JettyFactory 代碼啟動雙保險。start/stop 目標,自動化集成測試零侵入。java -jar 即服務,容器化部署極簡 (Dockerfile 僅需 ENTRYPOINT ["java","-jar","app.war"])。掌握這三套配置,讓 Jetty 真正成為從開發筆記本到 Kubernetes 生產集群的「隨處可運行」容器。