1. 簡介
在使用 Spring 在 Web 應用程序中,我們有幾種組織應用程序上下文並將其連接起來的選項。
在本文中,我們將分析和解釋 Spring 提供的最常見選項。
注意:本文中大部分章節是非 Spring Boot 解決方案;它們已被棄用,並且與舊版本的 Spring 兼容。
2. 根 Web 應用程序上下文
每個 Spring Web 應用程序都與它的生命週期關聯的一個應用程序上下文——根 Web 應用程序上下文。
這是一個在 Spring Web MVC 之前出現的舊特性,因此它與任何 Web 框架技術無關。
當應用程序啓動時,上下文啓動,並在停止時被銷燬,這要歸功於 Servlet 上下文監聽器。最常見的上下文類型也可以在運行時刷新,儘管並非所有 ApplicationContext 實現都具有此功能。
Web 應用程序中的上下文始終是一個 WebApplicationContext 的實例。它是一個擴展 ApplicationContext 的接口,並提供對 ServletContext 的訪問接口。
總的來説,應用程序通常無需關心這些實現細節:根 Web 應用程序上下文只是一個定義共享 Bean 的中心化位置。
2.1. <em>ContextLoaderListener</em>>
Web 應用程序上下文,如前文所述,由 org.springframework.web.context.ContextLoaderListener 類的一個監聽器進行管理,該監聽器屬於 <em>spring-web</em> 模塊。
默認情況下,監聽器將從 <em>/WEB-INF/applicationContext.xml</em> 加載 XML 應用程序上下文。 但是,這些默認值可以更改。 例如,我們可以使用 Java 註解而不是 XML。
我們可以通過在 Web 應用程序描述符(<em>web.xml</em> 文件)中或在 Servlet 3.x 環境中程序化方式來配置此監聽器。
在後續部分中,我們將詳細研究這些選項。
2.2. 使用 web.xml 和 XML 應用上下文
當使用 web.xml 時,我們按照常規配置監聽器:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>我們可以通過 contextConfigLocation 參數指定 XML 上下文配置的替代位置:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/rootApplicationContext.xml</param-value>
</context-param>或者多個位置,用逗號分隔:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/context1.xml, /WEB-INF/context2.xml</param-value>
</context-param>我們甚至可以使用模式:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/*-context.xml</param-value>
</context-param>在任何情況下,僅定義一個上下文是通過將從指定位置加載的所有 Bean 定義組合而得的。
2.3. 使用 web.xml 和 Java 應用上下文
我們可以指定除了默認基於 XML 的上下文類型之外的其他類型。例如,讓我們看看如何使用 Java 註解配置。
我們使用 contextClass 參數來告訴監聽器實例化哪種類型的上下文:
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>任何類型的上下文都可能具有默認配置位置。 在我們的例子中,AnnotationConfigWebApplicationContext 沒有默認配置位置,因此我們需要提供它。
我們可以列出一種或多種標註類:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
com.baeldung.contexts.config.RootApplicationConfig,
com.baeldung.contexts.config.NormalWebAppConfig
</param-value>
</context-param>我們也可以告知掃描器掃描一個或多個軟件包的上下文:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.baeldung.bean.config</param-value>
</context-param>當然,我們還可以混合使用這兩種選項。
2.4. 使用 Servlet 3.x 進行程序化配置
Servlet 3.x 版本的 API 使得通過 web.xml 文件進行配置完全可選。 庫可以提供其 Web 片段,這些是 XML 配置片段,可以註冊監聽器、過濾器、Servlet 等。
用户還可以訪問一個 API,允許程序定義 Servlet 應用程序的每個元素。
spring-web 模塊利用這些功能,並提供 API 以在應用程序啓動時註冊組件。
Spring 會掃描應用程序的類路徑,查找 org.springframework.web.WebApplicationInitializer 類的實例。 這是一個接口,具有一個方法 void onStartup(ServletContext servletContext) throws ServletException,該方法在應用程序啓動時被調用。
現在,讓我們看看如何使用此功能來創建我們之前見過的相同類型的根 Web 應用程序上下文。
2.5. 使用 Servlet 3.x 和 XML 應用上下文
讓我們從 XML 上下文開始,就像在第 2.2 節中所做的那樣。
我們將實現上述的 onStartup 方法:
public class ApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
//...
}
}讓我們逐行分析實現過程。
首先,我們創建一個根上下文。由於我們使用 XML,因此它必須是基於 XML 的應用程序上下文,並且由於我們身處 Web 環境中,它還需要實現 WebApplicationContext。
因此,第一行是 contextClass 參數的顯式版本,我們之前已經遇到過它,它決定了我們使用哪個特定的上下文實現。
XmlWebApplicationContext rootContext = new XmlWebApplicationContext();然後,在第二行,我們指定了從哪裏加載其 Bean 定義的上下文。 再次強調,setConfigLocations 是 contextConfigLocation 參數在 <web.xml> 中的程序化對應關係:
rootContext.setConfigLocations("/WEB-INF/rootApplicationContext.xml");最後,我們創建一個 ContextLoaderListener,並將其與servlet容器註冊。正如我們所見,ContextLoaderListener 具有一個合適的構造函數,它接受一個 WebApplicationContext 並將其提供給應用程序:
servletContext.addListener(new ContextLoaderListener(rootContext));2.6. 使用 Servlet 3.x 和 Java 應用上下文
如果希望使用基於註解的上下文,我們可以修改上一節的代碼片段,使其實例化 AnnotationConfigWebApplicationContext。
但是,讓我們來看一個更具體的實現方法,以達到相同的效果。
WebApplicationInitializer 類是我們之前見過的通用接口。 實際上,Spring 提供了幾個更具體的實現,包括一個抽象類 AbstractContextLoaderInitializer。
它的工作,正如其名稱所暗示的,是創建 ContextLoaderListener 並將其註冊到servlet容器中。
我們只需要告訴它如何構建根上下文:
public class AnnotationsBasedApplicationInitializer
extends AbstractContextLoaderInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext rootContext
= new AnnotationConfigWebApplicationContext();
rootContext.register(RootApplicationConfig.class);
return rootContext;
}
}在這裏,我們可以看到我們不再需要註冊 ContextLoaderListener,這省去了不少冗餘代碼。
此外,還請注意使用針對 AnnotationConfigWebApplicationContext 的 register 方法,而不是更通用的 setConfigLocations。通過調用該方法,我們可以將單個帶有 @Configuration 註解的類註冊到上下文中,從而避免包掃描。
3. Dispatcher Servlet 環境
現在,我們將重點關注另一種類型的應用程序環境。這次我們將討論一種特定於 Spring MVC 的特性,而不是 Spring 的通用 Web 應用程序支持的一部分。
Spring MVC 應用程序至少有一個 Dispatcher Servlet 配置(但可能不止一個,稍後我們將討論這種情況)。這是接收傳入請求、將其分發到適當的控制器方法並返回視圖的 Servlet。
每個 DispatcherServlet 都與一個應用程序環境相關聯。定義的 Bean 將配置 Servlet 並定義 MVC 對象,例如控制器和視圖解析器。
讓我們首先看一下如何配置 Servlet 的環境。稍後我們將深入探討一些細節。
3.1. 使用 web.xml 和 XML 應用上下文
DispatcherServlet 通常在 web.xml 中聲明,並指定名稱和映射。
<servlet>
<servlet-name>normal-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>normal-webapp</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>除非另有説明,否則 Servlet 的名稱將用於確定要加載的 XML 文件。在我們的示例中,我們將使用文件 WEB-INF/normal-webapp-servlet.xml。
我們也可以像 ContextLoaderListener 那樣指定一個或多個 XML 文件的路徑:
<servlet>
...
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/normal/*.xml</param-value>
</init-param>
</servlet>3.2. 使用 web.xml 和 Java 應用上下文
當我們想要使用不同類型的上下文時,方法與使用 ContextLoaderListener 類似,即再次執行以下操作:指定 contextClass 參數以及合適的 contextConfigLocation:
<servlet>
<servlet-name>normal-webapp-annotations</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.baeldung.contexts.config.NormalWebAppConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>3.3. 使用 Servlet 3.x 和 XML 應用上下文
再次強調,我們將探討兩種方法來程序化聲明 DispatcherServlet,並將一種應用於 XML 上下文,另一種應用於 Java 上下文。
正如我們之前所見,我們需要實現 onStartup 方法。但這一次,我們將創建並註冊一個 dispatcher servlet 也是如此:
XmlWebApplicationContext normalWebAppContext = new XmlWebApplicationContext();
normalWebAppContext.setConfigLocation("/WEB-INF/normal-webapp-servlet.xml");
ServletRegistration.Dynamic normal
= servletContext.addServlet("normal-webapp",
new DispatcherServlet(normalWebAppContext));
normal.setLoadOnStartup(1);
normal.addMapping("/api/*");我們可以很容易地將上述代碼與等效的 web.xml 配置元素進行比較。
3.4. 使用 Servlet 3.x 和 Java 應用上下文
現在,我們將使用一種專門的實現,即 <emWebApplicationInitializer</em>> 的實現,配置基於註解的上下文:<emAbstractDispatcherServletInitializer</em>>。
這是一種抽象類,除了創建之前所見到的根 Web 應用上下文外,還允許我們以最少的樣板代碼註冊一個 Dispatcher Servlet:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext secureWebAppContext
= new AnnotationConfigWebApplicationContext();
secureWebAppContext.register(SecureWebAppConfig.class);
return secureWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/s/api/*" };
}在這裏,我們可以看到一種創建與servlet關聯上下文的方法,與我們之前創建根上下文時所見的方法完全相同。我們還提供了一種指定servlet映射的方法,類似於在<em>web.xml</em>中進行的配置。
4. 父上下文與子上下文
到目前為止,我們已經看到了兩種主要類型的上下文:根 Web 應用程序上下文和分發器服務上下文。那麼,我們可能會問自己一個問題:這些上下文相關嗎?
事實證明,是的,它們是相關的。事實上,根上下文是每個分發器服務上下文的父上下文。因此,在根 Web 應用程序上下文中定義的 Bean 可以被每個分發器服務上下文可見,但反之則不然。
通常,根上下文用於定義服務 Bean,而分發器上下文包含那些專門與 MVC 相關的 Bean。
請注意,我們還看到了如何通過編程方式創建分發器服務上下文。如果我們手動設置其父上下文,則 Spring 不會覆蓋我們的決定,因此本節不再適用。
在簡單的 MVC 應用程序中,僅有一個與單個分發器服務關聯的上下文就足夠了。無需使用過於複雜的解決方案!
儘管如此,父子上下文關係在有多個分發器服務配置時變得有用。但我們應該何時花費精力來擁有多個分發器服務呢?
一般來説,我們聲明多個分發器服務當我們需要多個 MVC 配置集時。例如,我們可能同時擁有 REST API 以及傳統的 MVC 應用程序或網站的未受保護和受保護部分:
請注意:當我們擴展 AbstractDispatcherServletInitializer(參見第 3.4 節)時,我們註冊了根 Web 應用程序上下文和單個分發器服務。
因此,如果我們想要多個 Servlet,則需要多個 AbstractDispatcherServletInitializer 實現。但是,我們只能定義一個根上下文,或者應用程序將無法啓動。
幸運的是,createRootApplicationContext 方法可以返回 null。因此,我們可以有一個 AbstractContextLoaderInitializer 並且有多個 AbstractDispatcherServletInitializer 實現,而無需創建根上下文。在這種情況下,建議使用 @Order 顯式地對初始器進行排序。
此外,請注意,AbstractDispatcherServletInitializer 會將 Servlet 註冊在一個給定的名稱下(dispatcher),當然,我們不能有具有相同名稱的多個 Servlet。因此,我們需要覆蓋 getServletName:
@Override
protected String getServletName() {
return "another-dispatcher";
}5. 父子上下文示例
假設我們應用程序中有兩個區域,例如一個對世界公開的區域和一個具有不同 MVC 配置的受保護區域。在這裏,我們只需定義兩個控制器,它們分別輸出不同的消息。
假設某些控制器需要一個服務,該服務持有大量資源;一個常見的例子是持久化。那麼,我們希望只實例化該服務一次,以避免其資源使用量的雙倍,因為我們遵循“不要重複自己”的原則!
現在,讓我們繼續示例。
5.1. 共享服務
在我們的“Hello World”示例中,我們選擇了一個更簡單的問候服務,而不是持久化:
package com.baeldung.contexts.services;
@Service
public class GreeterService {
@Resource
private Greeting greeting;
public String greet() {
return greeting.getMessage();
}
}我們將在根 Web 應用程序上下文中通過組件掃描來聲明服務:
@Configuration
@ComponentScan(basePackages = { "com.baeldung.contexts.services" })
public class RootApplicationConfig {
//...
}我們可能更喜歡XML:
<context:component-scan base-package="com.baeldung.contexts.services" />5.2. 控制器
讓我們定義兩個簡單的控制器,它們使用服務並輸出問候語:
package com.baeldung.contexts.normal;
@Controller
public class HelloWorldController {
@Autowired
private GreeterService greeterService;
@RequestMapping(path = "/welcome")
public ModelAndView helloWorld() {
String message = "<h3>Normal " + greeterService.greet() + "</h3>";
return new ModelAndView("welcome", "message", message);
}
}
//"Secure" Controller
package com.baeldung.contexts.secure;
String message = "<h3>Secure " + greeterService.greet() + "</h3>";如我們所見,控制器位於兩個不同的包中,並打印不同的消息:一個顯示“normal”,另一個顯示“secure”。
5.3. Dispatcher Servlet 環境
正如我們之前所説,我們將擁有兩個不同的Dispatcher Servlet環境,一個用於每個控制器。 讓我們在Java中定義它們:
//Normal context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.baeldung.contexts.normal" })
public class NormalWebAppConfig implements WebMvcConfigurer {
//...
}
//"Secure" context
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.baeldung.contexts.secure" })
public class SecureWebAppConfig implements WebMvcConfigurer {
//...
}或者,如果更喜歡,以XML格式:
<!-- normal-webapp-servlet.xml -->
<context:component-scan base-package="com.baeldung.contexts.normal" />
<!-- secure-webapp-servlet.xml -->
<context:component-scan base-package="com.baeldung.contexts.secure" />5.4. 整合所有組件
現在我們已經獲得了所有組件,只需要告訴 Spring 將它們連接起來。請記住,我們需要加載根上下文並定義兩個 Dispatcher Servlet。雖然我們已經看到了多種實現方法,但現在我們將重點關注兩個場景:Java 和 XML。 我們先從 Java 開始。
我們將定義一個 AbstractContextLoaderInitializer 以加載根上下文:
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext rootContext
= new AnnotationConfigWebApplicationContext();
rootContext.register(RootApplicationConfig.class);
return rootContext;
}
然後,我們需要創建兩個 Servlet。因此,我們將定義兩個繼承自 AbstractDispatcherServletInitializer 的子類。首先,"正常" 的一個:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext normalWebAppContext
= new AnnotationConfigWebApplicationContext();
normalWebAppContext.register(NormalWebAppConfig.class);
return normalWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/api/*" };
}
@Override
protected String getServletName() {
return "normal-dispatcher";
}
然後,"安全"版本會加載不同的上下文並映射到不同的路徑:
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext secureWebAppContext
= new AnnotationConfigWebApplicationContext();
secureWebAppContext.register(SecureWebAppConfig.class);
return secureWebAppContext;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/s/api/*" };
}
@Override
protected String getServletName() {
return "secure-dispatcher";
}我們已經完成了!我們剛剛應用了之前章節所涉及的內容。
我們可以同樣地使用 web.xml,同樣地,通過將我們討論過的各個部分組合起來來實現。
定義根應用程序上下文:
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
一個“正常”的調度器上下文:
<servlet>
<servlet-name>normal-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>normal-webapp</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
最後,一個“安全”的環境:
<servlet>
<servlet-name>secure-webapp</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>secure-webapp</servlet-name>
<url-pattern>/s/api/*</url-pattern>
</servlet-mapping>6. 組合多個上下文
除了父子關係之外,還可以通過分割大型上下文並更好地分離不同的關注點來組合多個配置位置。我們之前已經見過一個例子:當我們使用contextConfigLocation指定多個路徑或包時,Spring會將所有Bean定義組合成一個單一的上下文,就像它們被編寫在一個單一的XML文件或Java類中一樣。
然而,我們也可以通過其他手段來實現類似的效果,甚至可以結合使用不同的方法。讓我們來審視我們的選擇。
一種可能性是組件掃描,我們在另一篇文章中對此進行了解釋。
6.1. 將上下文導入到另一個上下文中
或者,我們可以將上下文定義導入到另一個上下文中。根據具體情況,我們有不同類型的導入。
在 Java 中導入 @Configuration 類:
@Configuration
@Import(SomeOtherConfiguration.class)
public class Config { ... }加載其他類型的資源,例如 Java 中的 XML 上下文定義:
@Configuration
@ImportResource("classpath:basicConfigForPropertiesTwo.xml")
public class Config { ... }最後,在一個 XML 文件中包含另一個 XML 文件:
<import resource="greeting.xml" />因此,我們有許多方法來組織服務、組件、控制器等,這些元素協同工作以創建我們的出色應用程序。 令人愉快的是,IDE 能夠理解所有這些!
7. Spring Boot Web 應用程序
Spring Boot 自動配置應用程序的組件,因此,通常不需要過多地考慮如何組織它們。
儘管如此,在底層,Boot 使用 Spring 的特性,包括我們之前所見的一些特性。讓我們看看一些值得注意的差異。
在嵌入式容器中運行的 Spring Boot Web 應用程序默認情況下不運行 WebApplicationInitializer。
如果需要,我們可以使用 SpringBootServletInitializer 或 ServletContextInitializer 來編寫相同的邏輯,具體取決於所選擇的部署策略。
但是,對於添加 Servlet、過濾器和監聽器(如本文所示),無需這樣做。 事實上,Spring Boot 自動將所有與 Servlet 相關的 Bean 註冊到容器中:
@Bean
public Servlet myServlet() { ... }這樣定義的對象將根據約定進行映射:過濾器會自動映射到 /*,即映射到每個請求。如果註冊一個 Servlet,則映射到 /;否則,每個 Servlet 映射到其 Bean 名稱。
如果以上約定不適用於我們,我們可以定義 FilterRegistrationBean, ServletRegistrationBean 或 ServletListenerRegistrationBean 代替。這些類允許我們控制註冊的細粒度方面。
8. 結論
在本文中,我們深入探討了構建和組織 Spring Web 應用程序的各種選項。
我們省略了一些功能,特別是 企業應用程序中的共享上下文支持,當時撰寫本文時,該功能仍 在 Spring 5 中缺失。