嵌入式 Tomcat 作為嵌入 Java 應用程序的庫, 你可以在 mvnrepository 下載 發行版Jar 以及源碼
https://mvnrepository.com/search?q=embed+tomcat
作為最基本的依賴, 你需要以下幾個庫
- Tomcat Embed Core
- Tomcat Embed Logging JULI
- Tomcat Annotations API
概念
- Connector: tomcat監聽相關的配置對象。
import org.apache.catalina.connector.Connector;
Connector connector = new Connector();
conn.setProperty("address", hostname); // 監聽地址
conn.setPort(port); // 監聽端口
// 關聯到tomcat實例,啟動監聽
tomcat.setConnector(connector);
tomcat.start();
tomcat.getServer().await();
- 工作目錄,絕對路徑,該目錄下必須有一個"webapps"目錄,同時Tomcat還會生成一個work目錄,因此建議使用當前目錄"."
tomcat.setBaseDir(new File(".").getAbsolutePath());
// 指定任意工作目錄,自動創建webapps目錄
private void configDir(String baseDir) {
File root = new File(baseDir);
if (!root.isDirectory() && !root.mkdirs()) {
throw new RuntimeException("請提供Tomcat工作目錄");
}
String path4root = root.getAbsolutePath();
tomcat.setBaseDir(path4root);
File webapps = new File(path4root + "/webapps");
if (!webapps.isDirectory() && !webapps.mkdirs()) {
throw new RuntimeException("無法創建webapps目錄");
}
}
- 上下文、上下文目錄
添加上下文時,Tomcat會在工作目錄下生成目錄work/Tomcat/{Hostname}/{contextPath}
相關代碼:
tomcat.setHostname("my_tomcat");
tomcat.addContext(contextPath, docBase) // docBase:您可以為此目錄或WAR文件指定絕對路徑名,或者相對於所屬主機的appBase目錄的相對路徑名,除非在server.xml中定義了Context元素,或者docBase不在主機的appBase下,否則不得設置此字段的值
- 上下文映射
context = tomcat.addContext("", new File(baseDir).getAbsolutePath());
Tomcat.addServlet(context, "default", new HelloServlet()); // HelloServlet : HttpServlet
context.addServletMappingDecoded("/", "default");
現在, 讓我們把 tomcat 跑起來
package develon.test;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
public final class Main {
File tmpDir = new File("F:\\Game\\tomcat");
Tomcat tomcat = new Tomcat();
public static void main(String[] args) throws Throwable {
new Main().init();
}
private void init() throws Throwable {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
tomcat.destroy();
} catch (LifecycleException e) {
e.printStackTrace();
}
})
);
test();
}
private void test() throws Throwable {
tomcat.setBaseDir(tmpDir.getAbsolutePath()); // 設置工作目錄
tomcat.setHostname("localhost"); // 主機名, 將生成目錄: {工作目錄}/work/Tomcat/{主機名}/ROOT
System.out.println("工作目錄: " + tomcat.getServer().getCatalinaBase().getAbsolutePath());
tomcat.setPort(80);
Connector conn = tomcat.getConnector(); // Tomcat 9.0 必須調用 Tomcat#getConnector() 方法之后才會監聽端口
System.out.println("連接器設置完成: " + conn);
// contextPath要使用的上下文映射,""表示根上下文
// docBase上下文的基礎目錄,用於靜態文件。相對於服務器主目錄必須存在 ({主目錄}/webapps/{docBase})
Context ctx = tomcat.addContext("", /*{webapps}/~*/ "/ROOT");
Tomcat.addServlet(ctx, "globalServlet", new HttpServlet() {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain");
response.setHeader("Server", "Embedded Tomcat");
try (Writer writer = response.getWriter()) {
writer.write("Hello, Embedded Tomcat!");
writer.flush();
}
}
});
ctx.addServletMappingDecoded("/", "globalServlet");
tomcat.start();
System.out.println("tomcat 已啟動");
tomcat.getServer().await();
}
}
tomcat 嵌入正常, 讓我們繼續, 如何令 tomcat 加載 Spring Framework ?
嵌入式 tomcat 集成 Spring 框架
package develon.tomc;
import java.util.HashSet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.SpringServletContainerInitializer;
public class Main {
Tomcat tomcat;
{
tomcat = new Tomcat();
// tomcat.setAddDefaultWebXmlToWebapp(false);
// tomcat.noDefaultWebXmlPath();
}
public void run() throws Throwable {
tomcat.setBaseDir("F:\\Game\\tomcat");
tomcat.setHostname("localhost");
tomcat.setPort(80);
// tomcat.enableNaming();
// tomcat.getHost().setAutoDeploy(false);
// tomcat.getEngine().setBackgroundProcessorDelay(-1);
Context ctx = tomcat.addContext("", "ROOT");
ctx.addLifecycleListener(new LifecycleListener() {
public void lifecycleEvent(LifecycleEvent event) {
// System.out.println(event.getLifecycle().getState().name());
if (event.getLifecycle().getState() == LifecycleState.STARTING_PREP) {
try {
new SpringServletContainerInitializer().onStartup(new HashSet<Class<?>>() {
private static final long serialVersionUID = 1L;
{
add(WebAppInitializer.class);
}
}, ctx.getServletContext());
} catch (Throwable e) {
e.printStackTrace();
}
}
}
});
// tomcat.init();
tomcat.getConnector();
tomcat.start();
tomcat.getServer().await();
}
public static void main(String[] args) throws Throwable {
new Main().run();
}
}
其中 WebAppInitializer
是繼承 AbstractAnnotationConfigDispatcherServletInitializer
的一個配置類
由於 AbstractAnnotationConfigDispatcherServletInitializer 繼承了 SpringServletContainerInitializer, 所以可以簡寫為
Context ctx = tomcat.addContext("", "ROOT");
ctx.addLifecycleListener(new LifecycleListener() {
public void lifecycleEvent(LifecycleEvent event) {
if (event.getLifecycle().getState() == LifecycleState.STARTING_PREP) {
try {
new WebAppInitializer().onStartup(ctx.getServletContext());
} catch (Throwable e) {
e.printStackTrace();
}
}
}
});
這種方式好像會報一個錯誤, 不過可以忽略它, 但是注意這是一個運行時異常, 我們最好捕獲 Throwable, 否則程序直接退出了
(經查, 是由於注射 dispacherServlet 兩次造成的, 實際上第一次已經注射完成了)
java.lang.IllegalStateException: Failed to register servlet with name 'dispatcher'. Check if there is another servlet registered under the same name.
at org.springframework.web.servlet.support.AbstractDispatcherServletInitializer.registerDispatcherServlet(AbstractDispatcherServletInitializer.java:90)
at org.springframework.web.servlet.support.AbstractDispatcherServletInitializer.onStartup(AbstractDispatcherServletInitializer.java:63)
at develon.tomc.Main$1.lifecycleEvent(Main.java:37)
然后我們還能用閉包進一步簡化程序, 並且把煩人的棧痕跡刪除
ctx.addLifecycleListener((LifecycleEvent event) -> {
if (event.getLifecycle().getState() == LifecycleState.STARTING_PREP) {
try {
new WebAppInitializer().onStartup(ctx.getServletContext());
} catch (Throwable e) {
// e.printStackTrace();
}
}
});
我用 kotlin 簡單地封裝了一個 EmbeddedTomcat 類
import org.apache.catalina.startup.Tomcat
import org.apache.catalina.Context
import org.apache.catalina.LifecycleState
class EmbeddedTomcat {
var tomcat: Tomcat = Tomcat()
var ctx: Context? = null
init {
}
/** 初始化嵌入式 tomcat */
fun init() {
tomcat.setBaseDir("""F:\\Game\\tomcat""")
tomcat.setHostname("localhost")
tomcat.setPort(80)
ctx = tomcat.addContext("", "ROOT")
}
/** 開始監聽服務 */
fun run() {
tomcat.getConnector()
tomcat.start()
tomcat.getServer().await()
}
/** 啟動 Spring 框架, 注射 DispatcherServlet */
fun spring() {
var tyusya = false
ctx?.addLifecycleListener {
if (tyusya == false && it.getLifecycle().getState() == LifecycleState.STARTING_PREP) {
println("開始注射 -> ${ it.getLifecycle().getState() }")
val sctx = ctx?.getServletContext()
try {
WebAppInitializer().onStartup(sctx)
println("完成")
tyusya = true
} catch(e: Throwable) {
println("失敗: ${ e.message }")
}
}
}
}
fun spring2() { // 調用了 removeLifecycleListener 移除 tomcat 生命周期監聽器
ctx?.addLifecycleListener(object : LifecycleListener {
override fun lifecycleEvent(it: LifecycleEvent) {
if (it.getLifecycle().getState() == LifecycleState.STARTING_PREP) {
println("開始注射 DispatcherServlet -> ${ it.getLifecycle().getState() }")
try {
WebAppInitializer().onStartup(ctx?.getServletContext())
println("注射完成")
ctx?.removeLifecycleListener(this)
} catch(e: Throwable) {
println("注射失敗: ${ e.message }")
}
}
}
})
}
}
fun main() {
val tomcat = EmbeddedTomcat()
tomcat.init()
tomcat.spring()
tomcat.run()
}
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer
import org.springframework.context.annotation.ComponentScan
@ComponentScan(basePackageClasses = [DefController::class])
class WebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses() = null
override fun getServletMappings() = arrayOf("/")
override fun getServletConfigClasses() = arrayOf(WebAppInitializer::class.java)
}