在spring整體框架的核心概念中,容器的核心思想是管理Bean的整個生命周期。但在一個項目中,Spring容器往往不止一個,最常見的場景就是在一個項目中引入Spring和SpringMVC這兩個框架,其本質就是兩個容器:Spring是根容器,SpringMVC是其子容器。關於這兩個容器的創建、聯系及區別也正是本文所關注的問題。
一、引子
Spring和SpringMVC作為Bean管理容器和MVC層的默認框架,已被眾多web應用采用。但是在實際應用中,初級開發者常常會因對Spring和SpringMVC的配置失當導致一些奇怪的異常現象,比如Controller的方法無法攔截、Bean被多次加載等問題,這種情況發生的根本原因在於開發者對Spring容器和SpringMVC容器之間的關系了解不夠深入,這也正是本文要闡述的問題。
二、Spring容器、SpringMVC容器與ServletContext之間的關系
在Web容器中配置Spring時,你可能已經司空見慣於web.xml文件中的以下配置代碼,下面我們以該代碼片段為基礎來了解Spring容器、SpringMVC容器與ServletContext之間的關系。要想理解這三者的關系,需要先熟悉Spring是怎樣在web容器中啟動起來的。Spring的啟動過程其實就是其Spring IOC容器的啟動過程。特別地,對於web程序而言,IOC容器啟動過程即是建立上下文的過程。
<web-app> ... <!-- 利用Spring提供的ContextLoaderListener監聽器去監聽ServletContext對象的創建,並初始化WebApplicationContext對象 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Context Configuration locations for Spring XML files(默認查找/WEB-INF/applicationContext.xml) --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- 配置Spring MVC的前端控制器:DispatchcerServlet --> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> ... </web-app>
1、Spring的啟動過程
(1). 對於一個web應用,其部署在web容器中,web容器提供其一個全局的上下文環境,這個上下文就是ServletContext,其為后面的spring IoC容器提供宿主環境;
(2). 在web.xml中會提供有contextLoaderListener。在web容器啟動時,會觸發容器初始化事件,此時contextLoaderListener會監聽到這個事件,其contextInitialized方法會被調用。在這個方法中,spring會初始化一個啟動上下文,這個上下文被稱為根上下文,即WebApplicationContext。WebApplicationContext是一個接口類,確切的說,其實際的實現類是XmlWebApplicationContext,它就是spring的IoC容器,其對應的Bean定義的配置由web.xml中的<context-param>標簽指定。在這個IoC容器初始化完畢后,Spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE為屬性Key,將其存儲到ServletContext中,便於獲取;
(3). ContextLoaderListener監聽器初始化完畢后,開始初始化web.xml中配置的Servlet,這個servlet可以配置多個,以最常見的DispatcherServlet為例,這個servlet實際上是一個標准的前端控制器,用以轉發、匹配、處理每個servlet請求。DispatcherServlet上下文在初始化的時候會建立自己的IoC上下文,用以持有spring mvc相關的bean。特別地,在建立DispatcherServlet自己的IoC上下文前,會利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先從ServletContext中獲取之前的根上下文(即WebApplicationContext)作為自己上下文的parent上下文。有了這個parent上下文之后,再初始化自己持有的上下文。這個DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化處理器映射、視圖解析等。這個servlet自己持有的上下文默認實現類也是mlWebApplicationContext。初始化完畢后,spring以與servlet的名字相關(此處不是簡單的以servlet名為Key,而是通過一些轉換,具體可自行查看源碼)的屬性為屬性Key,也將其存到ServletContext中,以便后續使用。這樣每個servlet就持有自己的上下文,即擁有自己獨立的bean空間,同時各個servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定義的那些bean。
2、Spring容器與SpringMVC的容器聯系與區別
ContextLoaderListener中創建Spring容器主要用於整個Web應用程序需要共享的一些組件,比如DAO、數據庫的ConnectionFactory等;而由DispatcherServlet創建的SpringMVC的容器主要用於和該Servlet相關的一些組件,比如Controller、ViewResovler等。它們之間的關系如下:
(1). 作用范圍
子容器(SpringMVC容器)可以訪問父容器(Spring容器)的Bean,父容器(Spring容器)不能訪問子容器(SpringMVC容器)的Bean。也就是說,當在SpringMVC容器中getBean時,如果在自己的容器中找不到對應的bean,則會去父容器中去找,這也解釋了為什么由SpringMVC容器創建的Controller可以獲取到Spring容器創建的Service組件的原因。
(2). 具體實現
在Spring的具體實現上,子容器和父容器都是通過ServletContext的setAttribute方法放到ServletContext中的。但是,ContextLoaderListener會先於DispatcherServlet創建ApplicationContext,DispatcherServlet在創建ApplicationContext時會先找到由ContextLoaderListener所創建的ApplicationContext,再將后者的ApplicationContext作為參數傳給DispatcherServlet的ApplicationContext的setParent()方法。也就是說,子容器的創建依賴於父容器的創建,父容器先於子容器創建。在Spring源代碼中,你可以在FrameServlet.Java中找到如下代碼:
wac.setParent(parent);
其中,wac即為由DisptcherServlet創建的ApplicationContext,而parent則為有ContextLoaderListener創建的ApplicationContext。
此后,框架又會調用ServletContext的setAttribute()方法將wac加入到ServletContext中。
三、Spring容器和SpringMVC容器的配置
在Spring整體框架的核心概念中,容器是核心思想,就是用來管理Bean的整個生命周期的,而在一個項目中,容器不一定只有一個,Spring中可以包括多個容器,而且容器間有上下層關系,目前最常見的一種場景就是在一個項目中引入Spring和SpringMVC這兩個框架,其實就是兩個容器:Spring是根容器,SpringMVC是其子容器。在上文中,我們提到,SpringMVC容器可以訪問Spring容器中的Bean,Spring容器不能訪問SpringMVC容器的Bean。但是,若開發者對Spring容器和SpringMVC容器之間的關系了解不夠深入,常常會因配置失當而導致同時配置Spring和SpringMVC時出現一些奇怪的異常,比如Controller的方法無法攔截、Bean被多次加載等問題。
在實際工程中,一個項目中會包括很多配置,根據不同的業務模塊來划分,我們一般思路是各負其責,明確邊界,即:Spring根容器負責所有其他非controller的Bean的注冊,而SpringMVC只負責controller相關的Bean的注冊,下面我們演示這種配置方案。
(1). Spring容器配置
Spring根容器負責所有其他非controller的Bean的注冊:
<!-- 啟用注解掃描,並定義組件查找規則 ,除了@controller,掃描所有的Bean -->
<context:component-scan base-package="cn.edu.tju.rico">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
</context:component-scan>
(2). SpringMVC容器配置
SpringMVC只負責controller相關的Bean的注冊,其中@ControllerAdvice用於對控制器進行增強,常用於實現全局的異常處理類:
<!-- 啟用注解掃描,並定義組件查找規則 ,mvc層只負責掃描@Controller、@ControllerAdvice -->
<!-- base-package 如果多個,用“,”分隔 -->
<context:component-scan base-package="cn.edu.tju.rico"
use-default-filters="false">
<!-- 掃描@Controller -->
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
<!--控制器增強,使一個Contoller成為全局的異常處理類,類中用@ExceptionHandler方法注解的方法可以處理所有Controller發生的異常 -->
<context:include-filter type="annotation"
expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>
在<context:component-scan>中可以添加use-default-filters,Spring配置中的use-default-filters用來指示是否自動掃描帶有@Component、@Repository、@Service和@Controller的類。默認為true,即默認掃描。如果想要過濾其中這四個注解中的一個,比如@Repository,可以添加<context:exclude-filter />子標簽,如下:
<context:component-scan base-package="cn.edu.tju.rico" scoped-proxy="targetClass" use-default-filters="true"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> </context:component-scan>
而<context:include-filter/>子標簽是用來添加掃描注解的:
<context:component-scan base-package="cn.edu.tju.rico" scoped-proxy="targetClass" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/> </context:component-scan>
四、Spring容器和SpringMVC容器的配置失當帶來的問題
1、問題描述
在一個項目中,想使用Spring AOP在Controller中切入一些邏輯,但發現不能切入到Controller的中,但可以切入到Service中。最初的配置情形如下:
1). Spring的配置文件application.xml包含了開啟AOP自動代理、Service掃描配置以及Aspect的自動掃描配置,如下所示:
<aop:aspectj-autoproxy/>
<context:component-scan base-package="cn.edu.tju.rico">
2). Spring MVC的配置文件spring-mvc.xml主要內容是Controller層的自動掃描配置。
<context:component-scan base-package="cn.edu.tju.rico.controller" />
3). 增強代碼為如下:
@Component @Aspect public class SecurityAspect { private static final String DEFAULT_TOKEN_NAME = "X-Token"; private TokenManager tokenManager; @Resource(name = "tokenManager") public void setTokenManager(TokenManager tokenManager) { this.tokenManager = tokenManager; } @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public Object execute(ProceedingJoinPoint pjp) throws Throwable { // 從切點上獲取目標方法 MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); // 若目標方法忽略了安全性檢查,則直接調用目標方法 if (method.isAnnotationPresent(IgnoreSecurity.class)) { System.out .println("method.isAnnotationPresent(IgnoreSecurity.class) : " + method.isAnnotationPresent(IgnoreSecurity.class)); return pjp.proceed(); } // 從 request header 中獲取當前 token String token = WebContext.getRequest().getHeader(DEFAULT_TOKEN_NAME); // 檢查 token 有效性 if (!tokenManager.checkToken(token)) { String message = String.format("token [%s] is invalid", token); throw new TokenException(message); } // 調用目標方法 return pjp.proceed(); } }
4). 需要被代理的Controller如下:
@RestController @RequestMapping("/tokens") public class TokenController { private UserService userService; private TokenManager tokenManager; public UserService getUserService() { return userService; } @Resource(name = "userService") public void setUserService(UserService userService) { this.userService = userService; } public TokenManager getTokenManager() { return tokenManager; } @Resource(name = "tokenManager") public void setTokenManager(TokenManager tokenManager) { this.tokenManager = tokenManager; } @RequestMapping(method = RequestMethod.POST) @IgnoreSecurity public Response login(@RequestParam("uname") String uname, @RequestParam("passwd") String passwd) { boolean flag = userService.login(uname, passwd); if (flag) { String token = tokenManager.createToken(uname); System.out.println("**** Token **** : " + token); return new Response().success("Login Success..."); } return new Response().failure("Login Failure..."); } @RequestMapping(method = RequestMethod.DELETE) @IgnoreSecurity public Response logout(@RequestParam("uname") String uname) { tokenManager.deleteToken(uname); return new Response().success("Logout Success..."); } }
在運行過程中,發現這樣配置並沒有起作用,AOP配置不生效,沒有生成TokenController的代理。
2、解決方案
由上一節可知,Spring容器先於SpringMVC容器進行創建,並且SpringMVC容器的創建依賴於Spring容器。在SpringMVC容器創建TokenController時,由於其沒有啟用AOP代理,並且父容器的配置與子容器配置的獨立性,導致SpringMVC容器沒有為TokenController生成代理,所以沒有生效。我們只需要在SpringMVC的配置文件中添加Aspect的自動掃描配置即可:
<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.hodc.sdk.controller" />
