動態

詳情 返回 返回

使用內嵌Tomcat - 動態 詳情

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,可以同時有結果:

  • 訪問 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-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 方法處理請求。
user avatar ligaai 頭像 superiorc 頭像 huichangkudehanbaobao 頭像
點贊 3 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.