0%

嵌入式Tomcat

前言

在上一篇中,我们下载了 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 增加如下依赖:

1
2
3
4
5
6
7
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) 查询嵌入式 Tomcat 的版本

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

1
2
3
4
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 文件,此时目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
├── main
│   ├── kotlin
│   │   └── servlet
│   │   └── HelloServlet.kt
│   ├── resources
│   └── webapp
│   └── WEB-INF
│   └── web.backup.xml
└── test
├── kotlin
│   └── servlet
│   └── Main.kt
└── resources

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

1
2
fun main() {
}

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

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

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

1
2
// 添加 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 资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 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 了:

1
2
3
4
5
6
7
8
9
10
11
// 创建默认的 Server
val server = tomcat.server

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

// 启动 Tomcat
server.start()

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

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

总结

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

欢迎关注我的其它发布渠道