1. 前言
了解 SpringBoot 的人对内嵌 Tomcat 应该不陌生,内嵌 Tomcat 是指将 Tomcat Servlet 容器直接集成到应用程序中,作为应用的一部分运行,而不是作为一个独立的外部服务器。
内嵌 Tomcat 通常通过添加相关的 Tomcat 依赖到项目中来实现。在 Java 应用启动时,Tomcat 也会随之启动,成为应用的一部分。这是通过编程方式创建和配置 Tomcat 的实例来完成的。应用可以完全控制 Tomcat 的配置,包括端口、连接器、会话管理、安全设置等。
优点
- 简化部署和运维:内嵌 Tomcat 无需单独安装和运行 Tomcat 服务器,简化了部署和运维流程。部署应用时,只需处理一个包含了所有内容的可执行 JAR 或 WAR 文件。
- 提高开发效率:开发者可以直接从 IDE 启动应用,无需部署到独立的服务器。这可以大幅提升开发和测试的效率。
- 环境一致性:内嵌 Tomcat 确保开发、测试和生产环境中使用的 Tomcat 配置和版本一致,减少了环境差异带来的问题。
- 灵活的配置:内嵌 Tomcat 允许通过代码配置所有服务器参数,提供了极高的配置灵活性。
目的是为了将应用运行起来,我们甚至可以自己写代码实现内嵌 Tomcat,用来运行应用代码,这在一些内部框架研发、测试插件等场景都很有作用。
2. SpringBoot 中应用
2.1. starter 依赖
当在项目中引入 spring-boot-starter-web
依赖时,Spring Boot 自动引入了内嵌 Tomcat 的依赖以及其他 Web 开发所需的组件。
这个 starter
包含了 spring-boot-starter-tomcat
,它负责引入内嵌 Tomcat 的核心库。
Spring Boot 的自动配置机制通过 @EnableAutoConfiguration
注解启动,关于内嵌 Tomcat,主要的自动配置类是 EmbeddedServletContainerAutoConfiguration
,它包含了一个内部类 EmbeddedTomcat,这个内部类用 @ConditionalOnClass(Tomcat.class)
注解标注,确保只有在 Tomcat 类库存在时才进行配置。
在这个配置类中,Spring Boot 配置了 Tomcat 的各种属性,比如端口号、会话超时设置、错误页面等。它也允许用户通过 application.properties
文件来覆盖默认配置。
2.2. Servlet 映射过程
在 Spring Boot 中,将 @RequestMapping
注解转换为能够在 Tomcat 中处理请求的 Servlet 的过程涉及多个组件和层。这一过程主要是由 Spring MVC 框架负责,而不是 Spring Boot 直接处理:
Spring Boot
: 负责自动配置和启动嵌入式 Tomcat 服务器Spring MVC
: 则处理请求映射到具体的方法。以下是详细的解释:
1. Spring MVC 和 DispatcherServlet
在 Spring MVC 中,DispatcherServlet
是一个中央 Servlet(继承自 HttpServlet
),它接收进来的 HTTP 请求,并将它们分发到相应的控制器上。这个 Servlet 是 MVC 模式的前端控制器(Front Controller),负责协调不同的请求处理器。
2. 自动配置
在 Spring Boot 应用中,DispatcherServlet
的配置和注册通常是自动完成的。Spring Boot 的自动配置机制会检测到 Spring MVC 的库,然后自动配置 DispatcherServlet
,并将其注册为 Bean。
3. 注册 DispatcherServlet
在 Spring Boot 中,DispatcherServlet
通常是作为应用上下文中的一个 Bean 自动注册的。这是通过 ServletRegistrationBean
实现的,它在内部使用 Tomcat 的 API 将 DispatcherServlet
注册到 Servlet 容器中。
4. 请求映射处理
当定义一个控制器类和方法,并使用 @RequestMapping
或其衍生注解(如 @GetMapping
, @PostMapping
等)标注时,Spring MVC 通过以下步骤处理这些映射:
- 扫描组件:Spring Boot 使用
@SpringBootApplication
注解,该注解包括了@ComponentScan
,它告诉 Spring 哪里去查找带有@Controller
或@RestController
等注解的类。 - 创建请求映射:当
DispatcherServlet
启动时,它会创建一个RequestMappingHandlerMapping
Bean,这个 Bean 负责查找所有带有@RequestMapping
注解的方法,并建立 URL 路径与方法之间的映射关系。 - 处理请求:当 HTTP 请求到达时,
DispatcherServlet
使用RequestMappingHandlerMapping
查找对应的处理器方法。然后,它调用相关的方法来处理请求,并将结果返回给客户端。
5. 从请求到响应的流程
- HTTP 请求被 Tomcat 接收,并传递给
DispatcherServlet
。 DispatcherServlet
查询RequestMappingHandlerMapping
以找到请求 URL 对应的控制器方法。- 控制器方法执行并返回结果(模型和视图信息)。
DispatcherServlet
将模型数据渲染到视图或直接将数据写回到响应体中(对于 REST API)。
2.3. 只部署一个Servlet
如上述,在 Spring Boot 中,只部署了一个主要的 DispatcherServlet
,而不是为每个 @RequestMapping
对应的方法单独部署一个 Servlet。这种设计是 Spring MVC 框架的核心部分,也是所谓的前端控制器模式的实现。
1. 前端控制器模式
DispatcherServlet
充当前端控制器,负责处理所有通过 HTTP 进入应用的请求。这种模式的主要优点是集中请求处理,使得管理和维护变得更加简单。通过这种方式,Spring MVC 可以有效地管理控制器映射、请求分发、视图解析等。
2. 如何工作的?
- 请求接收:所有进入应用的 HTTP 请求首先被
DispatcherServlet
接收。 - 请求映射:
DispatcherServlet
会查询内部的HandlerMapping
(通常是RequestMappingHandlerMapping
)来找出请求 URL 对应的控制器方法。 - 请求处理:一旦确定了处理请求的方法,
DispatcherServlet
将请求委托给相应的控制器(controller)。 - 返回处理:控制器处理完请求后,返回的数据被送回到
DispatcherServlet
,然后可能经过视图解析器(View Resolver)处理(如果是返回视图的话),或者直接将数据写回响应体(对于 RESTful 接口)。
3. 为什么不为每个方法部署一个 Servlet?
- 性能和资源管理:如果为每个
@RequestMapping
部署一个单独的 Servlet,将会创建大量的 Servlet 实例,这不仅会消耗更多的内存资源,还会增加服务器启动和运行时的复杂性。 - 维护和配置:管理大量的 Servlet 配置将是一项繁重的任务。而集中处理所有请求的
DispatcherServlet
可以使用统一的配置和拦截器,简化这些任务。 - Spring 的依赖注入:在只有一个 DispatcherServlet 的情况下,通常只需要一个 Spring 应用上下文。所有的 Bean 都在同一个容器中管理,确保了配置和服务的一致性。所有的 Bean 都可以互相引用,无需担心跨上下文的引用问题。当有多个 Servlet 时,每个 Servlet 可能会有自己的 Spring 应用上下文。虽然可以通过父子上下文来解决多个 Servlet 上下文的问题,但这又增加了配置的复杂性。
3. 使用
3.1. 运行 Servlet
1. maven依赖
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat Embedded -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
2. 创建 Servlet
创建一个Servlet
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.getWriter().println("<h1>Hello, Embedded Tomcat!</h1>");
}
}
3. 运行Tomcat
public class App {
public static void main(String[] args) throws LifecycleException, InterruptedException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
// 配置连接器参数
Connector connector = tomcat.getConnector();
connector.setURIEncoding("UTF-8");
connector.setProperty("connectionTimeout", "20000");
connector.setProperty("maxThreads", "200");
// 创建上下文和Servlet
Context context = tomcat.addContext("/", null);
Wrapper servletWrapper = Tomcat.addServlet(context, "myServlet", new MyServlet());
servletWrapper.setLoadOnStartup(1);
servletWrapper.addMapping("/hello");
// 启动 Tomcat
tomcat.start();
tomcat.getServer().await();
}
}
执行main方法之后,访问 http://localhost:8080/hello ,发现会输出 Servlet 中内容。
3.2. 静态资源文件
1. 静态资源
在 src/main/resources/static
目录下放置静态资源文件如下:
.
├── pom.xml
├── src
│ ├── main
│ │ ├── java ...
│ │ └── resources
│ │ ├── application.properties
│ │ ├── static
│ │ │ ├── images
│ │ │ │ └── logo.jpeg
│ │ │ ├── index.html
│ │ │ └── styles.css
1. 运行Tomcat
public class App {
public static void main(String[] args) throws LifecycleException, InterruptedException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
// 配置连接器参数
Connector connector = tomcat.getConnector();
connector.setURIEncoding("UTF-8");
connector.setProperty("connectionTimeout", "20000");
connector.setProperty("maxThreads", "200");
// 创建上下文和Servlet
Context context = tomcat.addContext("/", null);
Wrapper servletWrapper = Tomcat.addServlet(context, "myServlet", new MyServlet());
servletWrapper.setLoadOnStartup(1);
servletWrapper.addMapping("/hello");
// 静态资源目录,创建上下文
String webApp=new java.io.File("src/main/resources/static/").getAbsolutePath();
Context webContext = tomcat.addWebapp("/web/", webApp);
// 启动 Tomcat
tomcat.start();
tomcat.getServer().await();
}
}
运行了两个 Context,可以同时有结果:
- 访问 http://localhost:8080/hello ,发现会输出 Servlet 中内容。
- 访问 http://localhost:8080/web ,页面展示 index.html 内容,也可以访问目录下静态资源(如:http://localhost:8080/web/images/log.jpeg )
3.3. 动态部署 Servlet
上面的例子都是创建好 Context、Servlet,最后再启动 Tomcat。那启动 Tomcat 之后再部署 Context、Servlet呢?
1. 运行 Tomcat
public class App {
public static void main(String[] args) throws LifecycleException, InterruptedException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
// 配置连接器参数
Connector connector = tomcat.getConnector();
connector.setURIEncoding("UTF-8");
connector.setProperty("connectionTimeout", "20000");
connector.setProperty("maxThreads", "200");
// 创建上下文和Servlet
Context context = tomcat.addContext("/", null);
Wrapper servletWrapper = Tomcat.addServlet(context, "myServlet", new MyServlet());
servletWrapper.setLoadOnStartup(1);
servletWrapper.addMapping("/hello");
// 静态资源目录,创建上下文
String webApp=new java.io.File("src/main/resources/static/").getAbsolutePath();
Context webContext = tomcat.addWebapp("/web/", webApp);
// 启动 Tomcat
tomcat.start();
tomcat.getServer().await();
System.out.println("Tomcat Started!");
Thread.sleep(20000L);
Tomcat.addServlet(context, "myServlet1", new MyServlet())
.addMapping("/word");
System.out.println("/word Servlet deployed!");
}
}
可以发现在 Tomcat 启动之后,可以访问 /hello
,但是访问 /word
是 404。在等待 20秒之后,访问 word
正常了。
4. 代码说明
4.1. 创建 Contenxt方式
Tomcat.addWebapp()
和 Tomcat.addContext()
是用于在内嵌式 Tomcat 中创建 Web 应用上下文的两种方法,它们在用途和参数上有一些区别。理解这两者的区别对于选择适合的上下文创建方式非常重要。
4.1.1. Tomcat.addWebapp()
1. 用途
Tomcat.addWebapp()
方法用于部署一个完整的 Web 应用程序,它通常是基于文件系统的目录结构,例如一个标准的 WAR 文件解压后的目录结构。
2. 参数
contextPath
: Web 应用的上下文路径。例如,如果你设置为/app
,那么应用将可以通过http://localhost:8080/app
访问。docBase
: 应用的文档根目录。这是应用的物理路径,应该指向一个包含WEB-INF
目录的完整 Web 应用程序目录。
3. 适用场景
- 适合直接部署现有的 Web 应用程序目录。
- 自动处理
WEB-INF/web.xml
等标准配置文件。
4. 示例
Tomcat tomcat = new Tomcat();
String webappDirLocation = "src/main/webapp/";
Context ctx = tomcat.addWebapp("/myapp", new File(webappDirLocation).getAbsolutePath());
4.1.2. Tomcat.addContext()
1. 用途
Tomcat.addContext()
方法用于创建一个更为灵活的上下文,它不需要一个完整的 Web 应用程序目录结构。
2. 参数
contextPath
: Web 应用的上下文路径,类似于addWebapp()
。baseDir
: 上下文的基础目录,用于存储临时文件、会话数据等。它不一定需要是一个完整的 Web 应用程序目录。
3. 适用场景
- 适用于需要动态配置或不依赖于标准目录结构的应用。
- 需要手动添加和配置 Servlets、Filters、Listeners 等。
4. 示例
Tomcat tomcat = new Tomcat();
String baseDir = new File(".").getAbsolutePath();
Context ctx = tomcat.addContext("/myapp", baseDir);
// 手动添加 Servlet
Tomcat.addServlet(ctx, "myServlet", new MyServlet());
ctx.addServletMappingDecoded("/servlet", "myServlet");
前面例子中,创建 Servlet 用的是 addContext
,因为用不到上下文基础目录存储数据,就设置 null 使用虚拟路径。
创建静态文件目录 用的是 addWebapp
,因为是需要同解压后 WAR 包,希望通过 URL 直接映射文件路径。
4.2. addWebapp 部署Web应用
在使用 Tomcat.addWebapp()
部署 Web 应用时,docBase
指定的是 Web 应用的文档根目录。通常情况下,docBase
下的文件是可以通过 URL 访问的,但有一些重要的例外和规则需要了解。
1. docBase
目录下的文件访问
- 公开访问的文件:
docBase
目录中的静态文件(如 HTML、CSS、JavaScript、图片等)通常可以通过 URL 直接访问。例如,如果docBase
是/path/to/myapp
,并且该目录中有一个index.html
文件,那么可以通过http://localhost:8080/myapp/index.html
访问它。 - 受保护的目录:
WEB-INF
和META-INF
是两个特殊的目录,不能通过浏览器直接访问。这些目录中的内容是受保护的,不会被 Tomcat 直接暴露给客户端。
2.WEB-INF
和web.xml
的作用
WEB-INF
目录:- 受保护: 这个目录中的文件和子目录不能被客户端通过 HTTP 请求直接访问。
- 存储配置和资源: 该目录用于存放 Web 应用的配置文件(如
web.xml
)、类文件(在classes
子目录中)和依赖的 JAR 包(在lib
子目录中)。
web.xml
文件:- 部署描述符:
WEB-INF/web.xml
是 Java EE 规范定义的 Web 应用部署描述符,用于配置 Servlets、Filters、Listeners、初始化参数、URL 映射等。 - 应用配置: 在
web.xml
中,你可以定义哪些 URL 映射到哪些 Servlets,以及配置其他与请求处理相关的设置。
- 部署描述符:
3. 安全性和设计考虑
- 安全性: 由于
WEB-INF
目录不能直接访问,它常被用于存储不应直接暴露给客户端的文件,如应用配置文件和类文件。 - 设计规范: 遵循 Java EE 的设计规范,确保应用程序的结构符合标准,有助于提高可维护性和安全性。
4.3. 映射 Servlet 方式
在 Apache Tomcat 中,Wrapper
和 Context
是两个不同层次的组件,它们分别提供了不同的方法来处理 Servlet 映射。了解这两种方法的差异有助于选择适合特定情况的配置方式。下面我们将对比 Wrapper#addMapping
和 Context#addServletMappingDecoded
这两种方法:
4.3.1. Wrapper#addMapping
定义:
Wrapper
是 Tomcat 中代表一个单独的 Servlet 实例的组件。Wrapper#addMapping
方法直接在这个Wrapper
上添加 URL 映射。
用途:
- 当你需要为特定的 Servlet 实例添加一个或多个 URL 映射时使用。每个
Wrapper
对应一个 Servlet,因此addMapping
是针对单个 Servlet 的配置。
- 当你需要为特定的 Servlet 实例添加一个或多个 URL 映射时使用。每个
优点:
- 直接关联:映射直接关联到特定的 Servlet 实例,清晰明了。
- 简单直接:适用于配置简单,Servlet 数量不多的情况。
示例:
Wrapper wrapper = Tomcat.addServlet(context, "myServlet", new MyServlet()); wrapper.addMapping("/hello");
4.3.2. Context#addServletMappingDecoded
定义:
Context
代表一个完整的 Web 应用程序,包含多个 Servlet。Context#addServletMappingDecoded
方法在Context
级别添加 URL 映射到指定的 Servlet 名称。
用途:
- 用于在 Web 应用的上下文中配置多个 Servlet 的 URL 映射。适用于需要集中管理多个 Servlet 映射的场景。
优点:
- 集中管理:在同一个
Context
中管理所有 Servlet 的映射,便于维护和审查。 - 统一配置:有利于实现跨多个 Servlet 的配置共享和统一安全策略。
- 集中管理:在同一个
示例:
Context context = tomcat.addContext("/", new File(".").getAbsolutePath()); Tomcat.addServlet(context, "myServlet", new MyServlet()); context.addServletMappingDecoded("/hello", "myServlet");
4.3.3. 对比
层次不同:
Wrapper#addMapping
针对单个 Servlet 实例进行配置。Context#addServletMappingDecoded
在整个应用上下文中配置,影响多个 Servlet。
管理范围:
Wrapper#addMapping
更适合单个或数量较少的 Servlet,管理相对简单。Context#addServletMappingDecoded
更适合大型应用或需要集中管理多个 Servlet 映射的场景。
使用场景:
- 如果应用只有少数几个 Servlet,使用
Wrapper#addMapping
可能更直接有效。 - 如果应用结构复杂,或需要统一处理多个 Servlet 的映射和配置,使用
Context#addServletMappingDecoded
可能更合适。
- 如果应用只有少数几个 Servlet,使用
通过理解这两种方法的差异,可以根据具体的应用需求和架构选择最适合的方法来配置 Servlet 映射。
4.4. Context 和 Wrapper
在 Apache Tomcat 中,Context
和 Wrapper
组件共同管理 Servlet 的配置和请求映射,但它们的职责和处理方式有所不同。
4.4.1. Context
Context
是一个 Web 应用的容器,它管理着应用内的所有 Servlet、Filter、Listener 以及其他资源。在处理 Servlet mapping 的角度来看:
映射管理:
Context
维护一个映射表,这个表将 URL 模式映射到相应的Wrapper
。当一个 HTTP 请求到达时,Context
根据请求的 URL 来确定哪个Wrapper
应该处理这个请求。
部署描述符:
- 在标准的 Java Web 应用中,
Context
的配置通常通过WEB-INF/web.xml
文件进行,这个文件中定义了 Servlet、Servlet mapping、欢迎文件列表等。 - 还记得通过
Tomcat.addWebapp()
创建 Context 把,有提到可以对应一个 WAR 解压目录(包含WEB-INF/web.xml
)。所以通常 WAR 包中,一个 Context 就对应一个web.xml
文件,在文件内管理各个 Servlet及映射关系。
- 在标准的 Java Web 应用中,
4.4.2. Wrapper
Wrapper
是一个专门为单个 Servlet 设计的容器,它负责管理一个具体 Servlet 的生命周期和请求处理。在 Servlet mapping 的角度来看:
直接映射:
Wrapper
本身不直接处理 URL 到 Servlet 的映射,这是由Context
来管理的。但Wrapper
需要知道自己应该处理哪些请求,这通常是通过在Context
中为Wrapper
设置 URL 模式来实现的。
简化配置:
- 如果你只有一个或几个 Servlet 需要配置,使用
Wrapper
可以简化配置过程。每个Wrapper
对应一个 Servlet,可以直接通过程序代码(如在嵌入式 Tomcat 中)或简单的配置来设定。
- 如果你只有一个或几个 Servlet 需要配置,使用
4.4.3. Context 和 Wrapper 的关系
当 Tomcat 启动或部署一个 Web 应用时,它会解析 web.xml 文件,并基于这些定义创建相应的 Context 和 Wrapper 对象。
- Context 创建:每个 Web 应用有一个对应的 Context 实例,Context 是通过解析
web.xml
中的配置(如<context-param>
,<listener>
,<filter>
等)来配置的。 Wrapper 创建:
- 对于
web.xml
中每个<servlet>
元素,Tomcat 创建一个 Wrapper。 - 每个 Wrapper 负责一个特定的 Servlet 类的实例化和生命周期管理。
- 对于
映射处理:
- Context 根据 <servlet-mapping> 的定义,设置内部的映射表,将 URL 模式关联到正确的 Wrapper。
- 当请求到达时,Context 使用这个映射表确定哪个 Wrapper 应该处理该请求。
下面是一个简单的 web.xml 示例,演示如何定义 Servlet 和它的映射:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>ExampleServlet</servlet-name>
<servlet-class>com.example.ExampleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ExampleServlet</servlet-name>
<url-pattern>/example</url-pattern>
</servlet-mapping>
</web-app>
在这个例子中:
- 一个名为
ExampleServlet
的Servlet
被定义,与com.example.ExampleServlet
类相关联。 - 这个
Servlet
被映射到 URL 模式/example
。
当 Tomcat 处理这个配置时,它会为 ExampleServlet
创建一个 Wrapper
并在所属的 Context
中注册这个映射。
4.4.4. 映射关系的处理流程
请求到达:
- HTTP 请求到达 Tomcat 后,首先被
Connector
接收。
- HTTP 请求到达 Tomcat 后,首先被
定位 Context:
- 根据请求的 URL,Tomcat 通过
Engine
和Host
确定应该由哪个Context
处理这个请求。
- 根据请求的 URL,Tomcat 通过
映射到 Wrapper:
Context
查看自己维护的 URL 到Wrapper
的映射表,找到对应的Wrapper
。
Servlet 处理:
Wrapper
负责初始化其 Servlet(如果还未初始化),然后调用 Servlet 的service
方法处理请求。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。