嵌入式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 程序,它的启动流程如下:
- 启动一个 JVM 加载 Tomcat 的主类并执行它的
main()
方法, - Tomcat 加载
war
包并初始化 Servlet, - 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) 查询嵌入式 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
启动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 服务器,在开发过程中便于断点调试。