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) }