Jetty是一個用 Java 實作、開源、基于标準的,并且具有豐富功能的 Http 伺服器和 Web 容器。Jetty中應用最廣泛的一項功能就是可以作為嵌入式Web容器。
在開發階段,可以使用Jetty在Eclipse裡直接啟動應用,而不是像Tomcat那樣繁瑣,先把幾十兆應用打包,然後再複制到某個目錄後再啟動。
在測試階段,可以直接在測試用例中啟動Jetty,而不是先将應用打包部署到容器。
在運作階段,可以将war包配置成直接能夠運作的應用
本文将着重介紹如何配置使用Jetty的嵌入式Web容器功能,關于Jetty的基本配置和功能請參考http://www.ibm.com/developerworks/cn/web/wa-lo-jetty/
一、開發階段
1、使用maven啟動Jetty
我們修改了源碼的時候eclipse會自動編譯,Jetty Maven Plugin插件發現編譯檔案有變化後會自動更新到jetty容器中,非常友善我們進行開發。
首先定義Jetty的版本屬性
<properties>
<jetty.version>8.1.9.v20130131</jetty.version>
</properties>
然後引入Jetty依賴
<!-- jetty -->
<dependency>
<groupId>org.eclipse.jetty.aggregate</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
配置Jetty Maven Plugin插件,示例如下
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<systemProperties>
<systemProperty>
<name>spring.profiles.active</name>
<value>development</value>
</systemProperty>
</systemProperties>
<useTestClasspath>true</useTestClasspath>
<webAppConfig>
<contextPath>/${project.artifactId}</contextPath>
</webAppConfig>
</configuration>
</plugin>
該配置運作jetty并指定spring的profile為development,同時設定web應用的上下文位址與應用本身的artifactId一緻。
執行如下指令啟動Jetty,即可通過http://localhost:8080/${project.artifactId}通路Web應用。
mvn jetty:run
Jetty Maven Plugin插件支援多個maven goals,最常用的就是run,下面的參數支援大部分的goals
(1)配置Jetty容器(支援所有goals)
- <connectors>:可選參數,配置org.eclipse.jetty.server.Connector(Jetty端口監聽器)清單,如果不指定該參數,将會連接配接預設的8080端口。
- <jettyXml>:可選參數,指定Jetty配置檔案的路徑
- <scanIntervalSeconds>:可選參數,配置Web應用掃描的暫停時間,Web應用掃描如果發現修改了程式,會自動重新部署。該參數預設值為0,即不啟動熱部署
- <systemProperties>:可選參數,設定插件執行時的系統參數,比如上面的配置示例中指定了spring的profile為development,如果不設定該參數,就需要配置maven與spring的profile一緻,同時在mvn指令中增加-Pdevelopment選項,或者直接在spring配置檔案中設定spring.profiles.active為development
- <systemPropertiesFile>:可選參數,設定系統參數配置檔案的位置,将會批量執行其中的系統參數配置
- <loginServices>:可選參數,配置org.eclipse.jetty.security.LoginService實作類的清單。
- <requestLog>:可選參數,配置請求日志接口:org.eclipse.jetty.server.RequestLog的實作類,配置請求日志的處理格式,
比如org.mortbay.jetty.NCSARequestLog就是一個NCSA格式((美國)國家超級計算技術應用中心 (NCSA) 公用格式,是常用的标準日志格式)的實作。
(2)配置Web應用程式(不支援run-forked、stop兩個goals)
- <webApp>:從jetty6.1.6rc0起使用webAppConfig,web應用程式配置根節點
- <contextPath>:設定web應用程式的context路徑,預設情況下,它被設定成該項目的pom.xml的<artifactId>
- <descriptor>:設定web應用程式主配置檔案web.xml的路徑,預設該配置檔案位于WEB-INF目錄下
- <defaultsDescriptor>:設定先于web.xml執行的webdefault.xml配置檔案的路徑
- <overrideDescriptor>:設定在web.xml讀取之後執行的配置檔案,使用該參數可以覆寫或增加web.xml中的配置
- <tempDirectory>:Web應用的臨時目錄,Jetty可以在此目錄編譯jsp檔案或者複制jar包,預設路徑為${project.build.outputDirectory}/tmp
- <baseResource>:指定Web應用靜态資源所在的路徑,預設路徑為src/main/webapp
- <resourceBases>:指定多個Web應用靜态資源所在的路徑,使用逗号分隔
- <baseAppFirst>:可選參數,預設值為true,控制是否可以在Web應用的原始資源之前或之後疊加多個war包
- <jettyEnvXml>:可選參數,指定jetty-env.xml配置檔案的路徑
- <containerIncludeJarPattern>:jetty-8.1.x之後的版本可以使用,可選參數,配置加載到Jetty容器 Classloader中的Jar包的路徑或比對模式,符合條件的jar包将會被檢測META-INF、資源、tld和類的繼承關系
- <webInfIncludeJarPattern>:jetty-8.1.x之後的版本可以使用,可選參數,配置加載到Web應用程式的Classloader(WEB-INF classpath)中的Jar包的路徑或比對模式,符合條件的jar包将會被檢測META-INF、資源、tld和類的繼承關系
- <contextXml>:可選參數,指定context xml配置檔案的路徑
run goals将會啟動Jetty并運作應用程式,不需要應用程式編譯成war包。另外run還支援webapp節點的其它屬性:
- <classesDirectory>:Web應用程式類編譯後的路徑
- <testClassesDirectory>:Web應用程式單元測試類編譯後的路徑,預設值為${project.build.testOutputDirectory}.
- <useTestScope>:Jetty-7之前的版本參數名稱為useTestClasspath,如果設定為true,開啟測試模式,<testClassesDirectory>中指定的類及其依賴将首先被加載到classpath中,預設值為false
- <useProvidedScope>:如果設定為true,依賴範圍标示為“provided”的依賴将被加載到容器的classpath中,該參數很少使用。
- <webAppSourceDirectory>:Web應用程式靜态資源路徑,預設值為${basedir}/src/main/webapp
- <scanTargets>:配置除了插件自動掃描的位置外,其它需要掃描的目錄或檔案清單
- <scanTargetPatterns>:配置除了插件自動掃描的位置外,其它需要掃描的目錄或檔案的比對模式
- <skip>:預設值為false,如果設定為true,将會停止執行插件
Jetty Maven Plugin插件支援的其它goals簡介如下(詳見http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin):
- run-war:将Web應用程式打包成war包并部署到Jetty中。
- run-exploded:使用war exploded模式(檔案夾模式)将Web應用程式打包并部署到Jetty中
- deploy-war:功能與run-war類似,差別就是maven生命周期中不包含package階段
- run-forked:jetty-7.5.2之後的版本可用,強迫Jetty使用一個新的JVM啟動應用程式
- start:jetty-7.6.0之後的版本可用,一般在配合插件中的execution節點使用,test-compile階段之後才執行建構,確定必要的類及檔案都生成好了。一般用于內建測試時啟動Jetty,本文第二部分測試階段會有詳細介紹
- stop:關閉運作中的Jetty容器
2、在java中啟動Jetty
SpringSide4中封裝了Jetty的操作提供了工具類JettyFactory ,讓我們可以很簡單的啟動Jetty容器,JettyFactory代碼如下:
/**
* Copyright (c) 2005-2012 springside.org.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
*/
package org.springside.modules.test.jetty;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
import com.google.common.collect.Lists;
/**
* 建立Jetty Server的工廠類.
*
* @author calvin
*/
public class JettyFactory {
private static final String DEFAULT_WEBAPP_PATH = "src/main/webapp";
private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault-windows.xml";
/**
* 建立用于開發運作調試的Jetty Server, 以src/main/webapp為Web應用目錄.
*/
public static Server createServerInSource(int port, String contextPath) {
Server server = new Server();
// 設定在JVM退出時關閉Jetty的鈎子。
server.setStopAtShutdown(true);
SelectChannelConnector connector = new SelectChannelConnector();
connector.setPort(port);
// 解決Windows下重複啟動Jetty居然不報告端口沖突的問題.
connector.setReuseAddress(false);
server.setConnectors(new Connector[] { connector });
WebAppContext webContext = new WebAppContext(DEFAULT_WEBAPP_PATH, contextPath);
// 修改webdefault.xml,解決Windows下Jetty Lock住靜态檔案的問題.
webContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH);
server.setHandler(webContext);
return server;
}
/**
* 設定除jstl-*.jar外其他含tld檔案的jar包的名稱.
* jar名稱不需要版本号,如sitemesh, shiro-web
*/
public static void setTldJarNames(Server server, String... jarNames) {
WebAppContext context = (WebAppContext) server.getHandler();
List<String> jarNameExprssions = Lists.newArrayList(".*/jstl-[^/]*\\.jar$", ".*/.*taglibs[^/]*\\.jar$");
for (String jarName : jarNames) {
jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$");
}
context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
StringUtils.join(jarNameExprssions, '|'));
}
/**
* 快速重新啟動application,重載target/classes與target/test-classes.
*/
public static void reloadContext(Server server) throws Exception {
WebAppContext context = (WebAppContext) server.getHandler();
System.out.println("[INFO] Application reloading");
context.stop();
WebAppClassLoader classLoader = new WebAppClassLoader(context);
classLoader.addClassPath("target/classes");
classLoader.addClassPath("target/test-classes");
context.setClassLoader(classLoader);
context.start();
System.out.println("[INFO] Application reloaded");
}
}
JettyFactory包含三個方法
- createServerInSource:以src/main/webapp為Web應用目錄建立Jetty WebServer,確定jvm退出時關閉Jetty,在同一個端口啟動多個jetty時報告端口沖突,并解決了javascript、css等靜态檔案被jetty鎖定而不能修改的問題。
- setTldJarNames:Jetty,tomcat等web容器通常都會對classloader做擴充,Jetty中的org.mortbay.jetty.webapp.WebAppClassLoader負責加載一個Web應用context中的應用類。
Jetty的jsp處理引擎來自于Glassfish,要求JSF标簽必須位于Jetty容器的classpath中,不能位于Web應用的classpath中,而Jetty的WebAppClassLoader優先使用父classloader加載類,導緻tld檔案都被加載到父classloader中,在Jetty的classpath中根本掃描不到,是以會出現找不到tld檔案的情況。setTldJarNames方法可以設定将包含tld的jar包加載到Jetty的classpath中。
- reloadContext:重新加載Jetty的context
調用JettyFactory在Jetty中運作調試Maven Web應用的示例代碼如下:
package org.springside.examples.quickstart;
import org.eclipse.jetty.server.Server;
import org.springside.modules.test.jetty.JettyFactory;
import org.springside.modules.test.spring.Profiles;
/**
* 使用Jetty運作調試Web應用, 在Console輸入回車快速重新加載應用.
*
* @author calvin
*/
public class QuickStartServer {
public static final int PORT = 8080;
public static final String CONTEXT = "/quickstart";
public static final String[] TLD_JAR_NAMES = new String[] { "sitemesh", "spring-webmvc", "shiro-web",
"springside-core" };
public static void main(String[] args) throws Exception {
// 設定Spring的profile
Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT);
// 啟動Jetty
Server server = JettyFactory.createServerInSource(PORT, CONTEXT);
JettyFactory.setTldJarNames(server, TLD_JAR_NAMES);
try {
server.start();
System.out.println("[INFO] Server running at http://localhost:" + PORT + CONTEXT);
System.out.println("[HINT] Hit Enter to reload the application quickly");
// 等待使用者輸入回車重載應用.
while (true) {
char c = (char) System.in.read();
if (c == '\n') {
JettyFactory.reloadContext(server);
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}
}
上段代碼還提供了通過捕獲在console中輸入的回車自動重新載入上下文,并重新載入Class檔案,提高了響應速度。
在執行main方法過程中如果發生如下錯誤:
class "javax.servlet.HttpConstraintElement"'s signer information does not match signer information of other classes in the same package
通過執行如下指令檢查依賴
mvn dependency:tree -Dverbose|grep servlet
檢查結果如圖

發現是因為Jetty8版本的包的依賴包org.eclipse.jetty.orbit.javax.servlet3.0.jar提供了javax.servlet.HttpConstraintElement類,而javax.servlet.servlet-api.jar的依賴包javax.servlet.javax.servlet-api-3.0.1.jar也提供了javax.servlet.HttpConstraintElement類,兩者發生了沖突。可以使用7.6.14.v20131031版本的Jetty解決此問題。
二、測試階段
在功能測試或內建測試階段,希望在測試開始時自動運作Jetty附加元件目進行測試,測試完成時停止Jetty容器。Jetty Maven Plugin插件可以幫助我們完成這種自動化工作。配置示例如下:
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<stopKey>foo</stopKey>
<stopPort>9999</stopPort>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
在上述配置中,通過execution來自定義運作階段:
- 在pre-integration-test階段運作start goals啟動Jetty容器
- 在post-integration-test階段運作stop goals停止Jetty容器。
使用<daemon>true</daemon>配置選項來預防Jetty無限期運作,迫使它隻在執行Maven時才運作。
三、運作階段
為了能夠建立可以直接運作的war包,需要把jetty jar包解開,将其中的class直接編譯到war包中,并需要在war中提供一個可以建立并運作Jetty的Main方法。本文提供兩種實作方法:
方法一
SpringSide4中提供了一種實作方法,稍加修改優化後步驟如下:
1、使用maven-assembly-plugin重新打包
maven-assembly-plugin插件能将應用程式打包成指定格式的分發包,更重要的是能夠自定義包含/排除指定的目錄或檔案。
為友善操作,單獨建立一個Maven Profile用于打包,配置如下:
<profile>
<id>standalone</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly-standalone.xml</descriptor>
</descriptors>
<archive>
<manifest>
<mainClass>org.springside.examples.showcase.Main</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
上述配置中,通過execution配置打包操作在package階段開始,引入assembly-standalone.xml檔案定義打包的規則,配置archive修改war包中的META-INF/Manifest.mf,替換main class為org.springside.examples.showcase.Main。
assembly-standalone.xml中的配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>standalone</id>
<formats>
<format>war</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<includes>
<include>org.eclipse.jetty*:*</include>
</includes>
<scope>provided</scope>
<unpack>true</unpack>
<unpackOptions>
<excludes>
<exclude>*</exclude>
<exclude>META-INF/*</exclude>
<exclude>about_files/*</exclude>
</excludes>
</unpackOptions>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.basedir}/target/${project.build.finalName}</directory>
<outputDirectory>/</outputDirectory>
<excludes>
<exclude>META-INF/**/*</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>${project.basedir}/target/classes</directory>
<includes>
<include>**/*/Main.class</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>
assembly-standalone.xml涉及到幾個關鍵點:
- formats指定打包的格式為war
- includeBaseDirectory為false表示建立的壓縮包不使用應用程式的名稱建立根目錄
- dependencySets用來定義選擇依賴并定義最終打包到什麼目錄,這裡我們通過include設定打包過程中包含Jetty的所有jar包,unpack設定将Jetty的jar包解壓縮,unpackOptions中的exclude将根目錄、META-INF、about_files裡的内容排除,避免簽名沖突同時減小war包的大小
- fileSets允許使用者通過檔案或目錄的粒度來控制打包,這裡第一個fileSet指定将${project.basedir}/target/${project.build.finalName}中的所有靜态資源打包到根目錄下,并排除META-INF目錄下的所有檔案,第二個fileSet将${project.basedir}/target/classes中的所有class及配置檔案打包到根目錄下,并将org.springside.examples.showcase.Main編譯後的Main.class添加到war包中
2、使用代碼建立Jetty容器
package org.springside.examples.quickstart;
import java.io.File;
import java.net.URL;
import java.security.ProtectionDomain;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
/**
* Main Class for standalone running.
*
* @author calvin
*/
public class Main {
public static void main(String[] args) throws Exception {
String contextPath = "/";
int port = Integer.getInteger("port", 8080);
Server server = createServer(contextPath, port);
try {
server.start();
server.join();
} catch (Exception e) {
e.printStackTrace();
System.exit(100);
}
}
private static Server createServer(String contextPath, int port) {
// use Eclipse JDT compiler
System.setProperty("org.apache.jasper.compiler.disablejsr199", "true");
Server server = new Server(port);
server.setStopAtShutdown(true);
ProtectionDomain protectionDomain = Main.class.getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
String warFile = location.toExternalForm();
WebAppContext context = new WebAppContext(warFile, contextPath);
context.setServer(server);
// 設定work dir,war包将解壓到該目錄,jsp編譯後的檔案也将放入其中。
String currentDir = new File(location.getPath()).getParent();
File workDir = new File(currentDir, "work");
context.setTempDirectory(workDir);
server.setHandler(context);
return server;
}
}
createServer方法負責建立Jetty服務,擷取war包路徑,建立context及工作目錄
main方法負責調用createServer方法建立Jetty服務,設定上下文路徑及啟動端口,并啟動Jetty服務,另外如果war包所在的路徑包含中文,則擷取路徑的代碼應修改為:
ProtectionDomain protectionDomain = Main.class.getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
location = java.net.URLDecoder.decode(location , "utf-8");
3、注意事項
通過以上配置,已經可以在Web應用程式内嵌入Jetty容器了,但還需要注意以下幾點
- Maven Pom中的Jetty依賴注意scope修改為provided,防止Jetty的Jar包被打到WEB-INF/lib中。
- 如果需要解析jsp頁面,需要在依賴中加入jsp-2.1-glassfish包的引用,注意其scope不能設定為provided
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1-glassfish</artifactId>
<version>2.1.v20100127</version>
</dependency>
- 由于Jetty隻會在容器的classpath中尋找jstl tags,是以需要注意将jstl包拆包到Jetty容器的classpath中,但是jetty-7 (7.6.9)、jetty-8 (8.1.9)、jetty-9 (9.0.0.M4)之後的版本内嵌了jstl包,不需要添加jstl包。
4、運作
執行如下指令将Web應用打包成war包,在${project.basedir}/target目錄下将會生成嵌入Jetty容器的war包。
mvn package -Pstandalone
通過如下指令運作war包。
Java -Xms2048m -Xmx2048m -XX:MaxPermSize=128m -jar xxx.war
方法二
方法一中主要是使用了maven-assembly-plugin進行自定義打包,除此之外還可以使用maven-war-plugin、maven-antrun-plugin、maven-dependency-plugin、maven-compiler-plugin共同實作建立可執行的war包
Maven POM配置示例如下:
<profile>
<id>standalone</id>
<build>
<finalName>quickstart</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<archive>
<manifest>
<mainClass>org.springside.examples.quickstart.Main</mainClass>
</manifest>
</archive>
<warName>${project.artifactId}-standalone</warName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>main-class-placement</id>
<phase>prepare-package</phase>
<configuration>
<target>
<move todir="${project.build.directory}/${project.artifactId}/">
<fileset dir="${project.build.directory}/classes/">
<include name="**/*/Main.class" />
</fileset>
</move>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.5.1</version>
<executions>
<execution>
<id>jetty-classpath</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>
<!-- remove some files in order to decrease size -->
<excludes>*, about_files/*, META-INF/*</excludes>
<!-- <excludeArtifactIds>jsp-api,jstl</excludeArtifactIds> -->
<outputDirectory>
${project.build.directory}/${project.artifactId}
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- to support compilation in linux -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<target>1.6</target>
<source>1.6</source>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</profile>
- maven-war-plugin負責打包,并設定Main方法實作類為org.springside.examples.quickstart.Main
- maven-antrun-plugin執行ant的target任務,将org.springside.examples.quickstart.Main類轉移到war包的跟路徑下
- maven-dependency-plugin負責将jetty相關的jar包進行拆包并重新輸出到war包根路徑下,以便重新打包
- maven-compiler-plugin負責編譯web應用
注意事項、org.springside.examples.showcase.Main類實作及運作方法同方法一。