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. 从请求到响应的流程
  1. HTTP 请求被 Tomcat 接收,并传递给 DispatcherServlet
  2. DispatcherServlet 查询 RequestMappingHandlerMapping 以找到请求 URL 对应的控制器方法。
  3. 控制器方法执行并返回结果(模型和视图信息)。
  4. DispatcherServlet 将模型数据渲染到视图或直接将数据写回到响应体中(对于 REST API)。

2.3. 只部署一个Servlet

如上述,在 Spring Boot 中,只部署了一个主要的 DispatcherServlet,而不是为每个 @RequestMapping 对应的方法单独部署一个 Servlet。这种设计是 Spring MVC 框架的核心部分,也是所谓的前端控制器模式的实现。

1. 前端控制器模式

DispatcherServlet 充当前端控制器,负责处理所有通过 HTTP 进入应用的请求。这种模式的主要优点是集中请求处理,使得管理和维护变得更加简单。通过这种方式,Spring MVC 可以有效地管理控制器映射、请求分发、视图解析等。

2. 如何工作的?
  1. 请求接收:所有进入应用的 HTTP 请求首先被 DispatcherServlet 接收。
  2. 请求映射DispatcherServlet 会查询内部的 HandlerMapping(通常是 RequestMappingHandlerMapping)来找出请求 URL 对应的控制器方法。
  3. 请求处理:一旦确定了处理请求的方法,DispatcherServlet 将请求委托给相应的控制器(controller)。
  4. 返回处理:控制器处理完请求后,返回的数据被送回到 DispatcherServlet,然后可能经过视图解析器(View Resolver)处理(如果是返回视图的话),或者直接将数据写回响应体(对于 RESTful 接口)。
3. 为什么不为每个方法部署一个 Servlet?
  1. 性能和资源管理:如果为每个 @RequestMapping 部署一个单独的 Servlet,将会创建大量的 Servlet 实例,这不仅会消耗更多的内存资源,还会增加服务器启动和运行时的复杂性。
  2. 维护和配置:管理大量的 Servlet 配置将是一项繁重的任务。而集中处理所有请求的 DispatcherServlet 可以使用统一的配置和拦截器,简化这些任务。
  3. 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,可以同时有结果:

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-INFMETA-INF 是两个特殊的目录,不能通过浏览器直接访问。这些目录中的内容是受保护的,不会被 Tomcat 直接暴露给客户端。
2. WEB-INFweb.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 中,WrapperContext 是两个不同层次的组件,它们分别提供了不同的方法来处理 Servlet 映射。了解这两种方法的差异有助于选择适合特定情况的配置方式。下面我们将对比 Wrapper#addMappingContext#addServletMappingDecoded 这两种方法:

4.3.1. Wrapper#addMapping

  1. 定义

    • Wrapper 是 Tomcat 中代表一个单独的 Servlet 实例的组件。Wrapper#addMapping 方法直接在这个 Wrapper 上添加 URL 映射。
  2. 用途

    • 当你需要为特定的 Servlet 实例添加一个或多个 URL 映射时使用。每个 Wrapper 对应一个 Servlet,因此 addMapping 是针对单个 Servlet 的配置。
  3. 优点

    • 直接关联:映射直接关联到特定的 Servlet 实例,清晰明了。
    • 简单直接:适用于配置简单,Servlet 数量不多的情况。
  4. 示例

    Wrapper wrapper = Tomcat.addServlet(context, "myServlet", new MyServlet());
    wrapper.addMapping("/hello");

4.3.2. Context#addServletMappingDecoded

  1. 定义

    • Context 代表一个完整的 Web 应用程序,包含多个 Servlet。Context#addServletMappingDecoded 方法在 Context 级别添加 URL 映射到指定的 Servlet 名称。
  2. 用途

    • 用于在 Web 应用的上下文中配置多个 Servlet 的 URL 映射。适用于需要集中管理多个 Servlet 映射的场景。
  3. 优点

    • 集中管理:在同一个 Context 中管理所有 Servlet 的映射,便于维护和审查。
    • 统一配置:有利于实现跨多个 Servlet 的配置共享和统一安全策略。
  4. 示例

    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 映射。

4.4. Context 和 Wrapper

在 Apache Tomcat 中,ContextWrapper 组件共同管理 Servlet 的配置和请求映射,但它们的职责和处理方式有所不同。

4.4.1. Context

Context 是一个 Web 应用的容器,它管理着应用内的所有 Servlet、Filter、Listener 以及其他资源。在处理 Servlet mapping 的角度来看:

  1. 映射管理:

    • Context 维护一个映射表,这个表将 URL 模式映射到相应的 Wrapper。当一个 HTTP 请求到达时,Context 根据请求的 URL 来确定哪个 Wrapper 应该处理这个请求。
  2. 部署描述符:

    • 在标准的 Java Web 应用中,Context 的配置通常通过 WEB-INF/web.xml 文件进行,这个文件中定义了 Servlet、Servlet mapping、欢迎文件列表等。
    • 还记得通过 Tomcat.addWebapp() 创建 Context 把,有提到可以对应一个 WAR 解压目录(包含 WEB-INF/web.xml)。所以通常 WAR 包中,一个 Context 就对应一个 web.xml 文件,在文件内管理各个 Servlet及映射关系。

4.4.2. Wrapper

Wrapper 是一个专门为单个 Servlet 设计的容器,它负责管理一个具体 Servlet 的生命周期和请求处理。在 Servlet mapping 的角度来看:

  1. 直接映射:

    • Wrapper 本身不直接处理 URL 到 Servlet 的映射,这是由 Context 来管理的。但 Wrapper 需要知道自己应该处理哪些请求,这通常是通过在 Context 中为 Wrapper 设置 URL 模式来实现的。
  2. 简化配置:

    • 如果你只有一个或几个 Servlet 需要配置,使用 Wrapper 可以简化配置过程。每个 Wrapper 对应一个 Servlet,可以直接通过程序代码(如在嵌入式 Tomcat 中)或简单的配置来设定。

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>

在这个例子中:

  • 一个名为 ExampleServletServlet 被定义,与 com.example.ExampleServlet 类相关联。
  • 这个 Servlet 被映射到 URL 模式 /example

当 Tomcat 处理这个配置时,它会为 ExampleServlet 创建一个 Wrapper 并在所属的 Context 中注册这个映射。

4.4.4. 映射关系的处理流程

  1. 请求到达:

    • HTTP 请求到达 Tomcat 后,首先被 Connector 接收。
  2. 定位 Context:

    • 根据请求的 URL,Tomcat 通过 EngineHost 确定应该由哪个 Context 处理这个请求。
  3. 映射到 Wrapper:

    • Context 查看自己维护的 URL 到 Wrapper 的映射表,找到对应的 Wrapper
  4. Servlet 处理:

    • Wrapper 负责初始化其 Servlet(如果还未初始化),然后调用 Servlet 的 service 方法处理请求。

KerryWu
641 声望159 粉丝

保持饥饿