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啓動時,它會創建一個RequestMappingHandlerMappingBean,這個 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方法處理請求。