在Spring總體框架的核心概念中,容器的核心思想是管理Bean的整個生命週期。但在一個項目中,Spring容器每每不止一個,最多見的場景就是在一個項目中引入Spring和SpringMVC這兩個框架,其本質就是兩個容器:Spring是根容器,SpringMVC是其子容器。關於這兩個容器的建立、聯繫及區別也正是本文所關注的問題。前端
喜歡本文、以爲本文對您有幫助的朋友別忘了點點贊和關注。持續更新更多幹貨java
Spring和SpringMVC做爲Bean管理容器和MVC層的默認框架,已被衆多web應用採用。可是在實際應用中,初級開發者經常會因對Spring和SpringMVC的配置失當致使一些奇怪的異常現象,好比Controller的方法沒法攔截、Bean被屢次加載等問題,這種狀況發生的根本緣由在於開發者對Spring容器和SpringMVC容器之間的關係瞭解不夠深刻,這也正是本文要闡述的問題。web
<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>
複製代碼
做用範圍spring
子容器(SpringMVC容器)能夠訪問父容器(Spring容器)的Bean,父容器(Spring容器)不能訪問子容器(SpringMVC容器)的Bean。也就是說,當在SpringMVC容器中getBean時,若是在本身的容器中找不到對應的bean,則會去父容器中去找,這也解釋了爲何由SpringMVC容器建立的Controller能夠獲取到Spring容器建立的Service組件的緣由。數據庫
具體實現express
在Spring的具體實現上,子容器和父容器都是經過ServletContext的setAttribute方法放到ServletContext中的。可是,ContextLoaderListener會先於DispatcherServlet建立ApplicationContext,DispatcherServlet在建立ApplicationContext時會先找到由ContextLoaderListener所建立的ApplicationContext,再將後者的ApplicationContext做爲參數傳給DispatcherServlet的ApplicationContext的setParent()方法。也就是說,子容器的建立依賴於父容器的建立,父容器先於子容器建立。在Spring源代碼中,你能夠在FrameServlet.java中找到以下代碼:spring-mvc
wac.setParent(parent);
複製代碼
其中,wac即爲由DisptcherServlet建立的ApplicationContext,而parent則爲有ContextLoaderListener建立的ApplicationContext。此後,框架又會調用ServletContext的setAttribute()方法將wac加入到ServletContext中。安全
在Spring總體框架的核心概念中,容器是核心思想,就是用來管理Bean的整個生命週期的,而在一個項目中,容器不必定只有一個,Spring中能夠包括多個容器,並且容器間有上下層關係,目前最多見的一種場景就是在一個項目中引入Spring和SpringMVC這兩個框架,其實就是兩個容器:Spring是根容器,SpringMVC是其子容器。在上文中,咱們提到,SpringMVC容器能夠訪問Spring容器中的Bean,Spring容器不能訪問SpringMVC容器的Bean。可是,若開發者對Spring容器和SpringMVC容器之間的關係瞭解不夠深刻,經常會因配置失當而致使同時配置Spring和SpringMVC時出現一些奇怪的異常,好比Controller的方法沒法攔截、Bean被屢次加載等問題。bash
在實際工程中,一個項目中會包括不少配置,根據不一樣的業務模塊來劃分,咱們通常思路是各負其責,明確邊界,即:Spring根容器負責全部其餘非controller的Bean的註冊,而SpringMVC只負責controller相關的Bean的註冊,下面咱們演示這種配置方案。mvc
(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 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的代理。
解決方案
由上一節可知,緣由有兩點:
所以,咱們只須要在SpringMVC的配置文件中添加Aspect的自動掃描配置便可實現所要的效果。此外,通常地,SpringMVC容器只管理Controller,剩下的Service、Repository 和 Component 由Spring容器只管理,不建議兩個容器上在管理Bean上發生交叉。所以,建議配置爲:
SpringMVC 配置:
<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.hodc.sdk.controller" />
複製代碼
Spring配置:
<context:annotation-config/>
<context:component-scan base-package="com.hodc.sdk">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
複製代碼