跳至主要內容

嵌入式Tomcat

guodongAndroid大约 4 分钟

前言

在上一篇中,我们下载了 Tomcat 服务器,并在 IDEA 中配置了 Tomcat Local Server,如果我们不想下载并配置Tomcat 服务器又想运行 Servlet,该如何是好呢?

开发环境

在上一篇的基础上,升级下 JDK、Gradle 和 Kotlin 的版本,本文不需要 Tomcat 服务器

  • IntelliJ IDEA 2022.2.4 (Ultimate Edition)
  • JDK 11
  • Gradle 7.5.1
  • Kotlin 1.8.21
  • Tomcat 10.1.13

嵌入式 Tomcat

实际上 Tomcat 也是一个 Java 程序,它的启动流程如下:

  1. 启动一个 JVM 加载 Tomcat 的主类并执行它的 main() 方法,
  2. Tomcat 加载 war 包并初始化 Servlet,
  3. Servlet 运行,提供服务。

从上面的流程可以看出,启动Tomcat其实是 JVM 加载 Tomcat 的主类并执行它的 main() 方法,我们可以把Tomcat 的 jar 包引入到项目里来,然后自己写一个 main() 方法来启动 Tomcat,然后让它加载项目里的 Servlet —— 这就是嵌入式Tomcat

修改 build.gradle.kts 增加如下依赖:

dependencies {
    providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")

+   testImplementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.13")

    testImplementation(kotlin("test"))
}

testImplementation 的依赖方式引入 tomcat-embed-core 包,版本为 10.1.13

接下来将在 test 包下编写自己的 main() 方法,因此以 testImplementation 的方式引入。

可以在 Maven Central (sonatype.com)open in new window 查询嵌入式 Tomcat 的版本

由于 10.1.13 版本的嵌入式 Tomcat 最低支持 Java 11,因此还需要修改 build.gradle.kts 中的 KotlinCompile Task 和项目配置,修改内容如下:

tasks.withType<KotlinCompile> {
-   kotlinOptions.jvmTarget = "1.8"
+   kotlinOptions.jvmTarget = "11"
}

修改项目配置中的 SDK 为 Java 11,Language level 为 SDK default

edit project structure

启动Tomcat

test 包下新建名为 servlet 的包并新建 Main.kt 的 Kotlin 文件,此时目录结构如下:

src
├── main
│   ├── kotlin
│   │   └── servlet
│   │       └── HelloServlet.kt
│   ├── resources
│   └── webapp
│       └── WEB-INF
│           └── web.backup.xml
└── test
    ├── kotlin
    │   └── servlet
    │       └── Main.kt
    └── resources

然后在其中增加以下 main() 函数:

fun main() {
}

main() 函数体里,首先通过以下代码创建一个 Tomcat 实例并配置服务器端口:

val tomcat = Tomcat()
tomcat.setPort(8080)

通过以下代码创建一个服务器上下文 Context 对象:

// 添加 Webapp 并返回一个 Context 实例
val context = tomcat.addWebapp("" /* contextPath */, File("src/main/webapp").absolutePath /* docBase */)

通过 tomcat.addWebapp(String, String) 方法添加 Webapp 并返回一个 Context 实例,其中 addWebapp() 方法的两个参数:

  • contextPath:表示 Webapp 的上下文映射,可以理解为 Webapp 的别名,
    • 比如:contextPath/hello,那么在浏览器访问时:http://localhost:8080/hello/any-servlet-url-pattern,
    • 参数只能是 空字符串 或者 / 开头且不以 / 结尾字符串 中的一种,空字符串 表示使用根上下文,如果不符合上述标准,Tomcat 内部会进行适当修正,
  • docBase:上下文的基准目录,用于查找/访问静态文件,
    • 目录必须存在且入参必须是绝对路径,

这里 contextPath 传入 空字符串 使用根上下文,目前项目没有静态文件,理论上现在 docBase 传入任意目录都可以,但最好还是传入 src/main/webapp,因为后续我们的静态文件还是会放到上述目录。

然后通过以下代码为上下文添加 Web 资源:

// 创建 WebResourceRoot 实例,表示 Webapp 的完整资源集
val root: WebResourceRoot = StandardRoot(context)

// 创建基于目录的 Webapp 子资源集
val dirResourceSet = DirResourceSet(
    root, /* WebResourceRoot root */
    "/WEB-INF/classes", /* String webAppMount: 资源目录要挂载到 Webapp 的路径 */
    File("build/classes/kotlin/main").absolutePath, /* String base: 资源目录的绝对路径 */
    "/" /* String internalPath: 资源目录的内部路径 */
)

// 将基于目录的子资源集添加到完整资源集的前置资源中
// Webapp 查找资源时按以下顺序:前置资源 -> 主要资源 -> JARs 资源 -> 后置资源
root.addPreResources(dirResourceSet)

// 将完整资源集添加到 Tomcat 上下文
context.resources = root

Webapp 需要很多资源才能运行,第一行代码创建一个 WebResourceRoot 实例,表示 Webapp 的完整资源集,完整资源集里面还有一些子资源集,比如:Servlet 的 classes、第三方的 JARs 包、JSPs 文件等。

Webapp 在查找 Servlet 时会从 /WEB-INF/classes 目录下查找,所以第二行代码将编译后的 Servlet classes 的目录 build/classes/kotlin/main 挂载到 /WEB-INF/classes 路径下。

Webapp 查找资源时按以下顺序:前置资源 -> 主要资源 -> JARs 资源 -> 后置资源,第三行代码将资源集添加到完整资源集的前置资源中。

然后将完整资源集添加到 Tomcat 上下文中。

接下来就可以启动 Tomcat 了:

// 创建默认的 Server
val server = tomcat.server

// 创建默认的 Service 和 Connector
tomcat.connector

// 启动 Tomcat
server.start()

// 等待接收 SHUTDOWN 命令, 阻塞主线程以保证主线程存活
server.await()

然后在浏览器中访问:http://localhost:8080/servlet 即可,在 Servlet 中还可以断点调试。

总结

嵌入式 Tomcat 不用本地下载并配置 Tomcat 服务器,在开发过程中便于断点调试。