Spring中的父子容器
背景
在很長的一段時間里面,關于Spring父子容器這個問題我一直沒太關注,但是上次同事碰見一個奇怪的bug于是我決定重新了解一下Spring中的父子容器。
項目是一個老的SSM項目,同事在使用AOP對Controller層的方法進行攔截做token驗證。這個功能在實際的開發(fā)項目中很常見對吧,估計大家都能輕易解決。但是問題就處在了AOP上面,根據AOP失效的八股文全部排查了一遍,問題還是沒有解決。但是神奇的問題出現(xiàn)了,我嘗試把切點放在Service中的方法AOP生效了。然后我看了下配置文件,發(fā)現(xiàn)了問題所在。
- root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.buydeem.container">
<context:exclude-filter type="regex" expression="com.buydeem.container.controller.*"/>
</context:component-scan>
<aop:aspectj-autoproxy/>
</beans>
- mvc-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.buydeem.container">
<context:exclude-filter type="regex" expression="com.buydeem.container.controller.*"/>
</context:component-scan>
</beans>
- TokenAspect
@Component
@Aspect
@Slf4j
public class TokenAspect {
@Pointcut("execution (public * com.buydeem.container.controller..*.*(..))")
//@Pointcut("execution (public * com.buydeem.container.service..*.*(..))")
public void point(){
}
@Before("point()")
public void before(){
log.info("before");
}
}
其實問題所在就是父子容器造成的,現(xiàn)在我們使用的SpringBoot中基本上不會出現(xiàn)問題,默認情況下SpringBoot中只會有一個容器,而在傳統(tǒng)的SSM架構中我們很可能會有兩個容器。在傳統(tǒng)的SSM架構中,我們會創(chuàng)建兩個配置文件,一個用來創(chuàng)建Controller層的容器通常是子容器,而Service和Dao層的容器通常就是父容器。
父子容器相關接口
在IOC容器時,Spring中通常會提到兩個頂級接口BeanFactory和ApplicationContext,這兩個都是IOC容器接口,相比BeanFactory而言,ApplicationContext提供了更強大的功能。
HierarchicalBeanFactory
該接口作為BeanFactory的子接口,它的定義如下:
public interface HierarchicalBeanFactory extends BeanFactory {
BeanFactory getParentBeanFactory();
boolean containsLocalBean(String name);
}
從它名稱可以看出,它是一個有層級的BeanFactory,它提供的兩個方法其中一個就是用來獲取父容器的。
ConfigurableBeanFactory
上面說了HierarchicalBeanFactory提供了獲取父容器的方法,那么父容器是怎么設置的呢?而設置父容器的方法則被定義在ConfigurableBeanFactory接口中。從名字可以看出它是一個可配置的BeanFactory,設置父容器的方法定義如下:
void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;
ApplicationContext
上面講了BeanFactory中獲取和設置父容器相關接口和方法,而ApplicationContext中同樣提供了一個方法用來獲取父容器。
ApplicationContext getParent();
ConfigurableApplicationContext
與BeanFactory中設置父容器一樣,ConfigurableApplicationContext提供了一個用來設置父容器的方法。
void setParent(@Nullable ApplicationContext parent);
特性
通過上面介紹我們明白了什么是父子容器,那么它有哪些特性呢?使用時需要注意什么呢?
示例代碼如下:
- 父容器配置
@Component
public class ParentService {
}
@Configuration
public class ParentContainerConfig {
@Bean
public ParentService parentService(){
return new ParentService();
}
}
- 子容器配置
@Component
public class ChildService {
}
@Configuration
public class ChildContainerConfig {
@Bean
public ChildService childService(){
return new ChildService();
}
}
子容器能獲取到父容器中的Bean
@Slf4j
public class App {
public static void main(String[] args) {
//父容器
ApplicationContext parentContainer = new AnnotationConfigApplicationContext(ParentContainerConfig.class);
//子容器
ConfigurableApplicationContext childContainer = new AnnotationConfigApplicationContext(ChildContainerConfig.class);
childContainer.setParent(parentContainer);
//從子容器中獲取父容器中的Bean
ParentService parentService = childContainer.getBean(ParentService.class);
log.info("{}",parentService);
//getBeansOfType無法獲取到父容器中的Bean
Map<String, ParentService> map = childContainer.getBeansOfType(ParentService.class);
map.forEach((k,v) -> log.info("{} => {}",k,v));
}
}
ParentService是父容器中的Bean,但是我們在子容器中卻能獲取到,這說明在子容器中是可以獲取到父容器中的Bean的,但是并不是所有方法都能,所以在使用時我們需要注意。這也解釋了一個問題,那就是在SSM架構中為什么我們能在Controller中獲取到Service,如果不是這個特性那我們的肯定是不行的。
父容器不能獲取子容器中的Bean
子容器能獲取到父容器中的Bean,但是父容器卻不能獲取到子容器中的Bean。
@Slf4j
public class App {
public static void main(String[] args) {
//父容器
ApplicationContext parentContainer = new AnnotationConfigApplicationContext(ParentContainerConfig.class);
//子容器
ConfigurableApplicationContext childContainer = new AnnotationConfigApplicationContext(ChildContainerConfig.class);
childContainer.setParent(parentContainer);
try {
ChildService childService = parentContainer.getBean(ChildService.class);
log.info("{}",childService);
}catch (NoSuchBeanDefinitionException e){
log.error(e.getMessage());
}
}
}
上面的代碼運行時會拋出異常,因為父容器是無法獲取到子容器中的Bean的。
SSM中的父子容器
回到我們最初的問題,在SSM中存在這兩個容器,這也是導致我們前面AOP失敗的原因。那么SSM中的父子容器是如何被創(chuàng)建和設置的呢?
web.xml
首先要解答這個問題我們需要先來看一下web.xml中的配置。
<?xml version="1.0" encoding="UTF-8"?>
<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">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
通常這個配置如上所示,我們需要關注的就兩分別為
ContextLoaderListener和DispatcherServlet。
父容器創(chuàng)建
其中ContextLoaderListener就是Servlet中的監(jiān)聽器,當Servlet容器啟動時就會調用contextInitialized()
方法進行初始化,該方法的實現(xiàn)如下:
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
而initWebApplicationConte()
的實現(xiàn)則在ContextLoader這個類中,該方法的實現(xiàn)如下:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
//創(chuàng)建WebApplicationContext容器
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//配置并刷新WebApplicationContext
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//將WebApplicationContext的引用保存到servletContext中(后面會用到)
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
雖然方法較長,但實際上我們需要關注的就三點:
創(chuàng)建容器
配置并刷新容器
將容器設置到servletContext中
子容器創(chuàng)建
子容器的創(chuàng)建我們需要關注的就是web.xml中DispatcherServlet
配置了,DispatcherServlet
說白了就是一個Servlet,當Servlet容器在實例化Servlet后就會調用其init()
方法就行初始化,而DispatcherServlet
的繼承如下圖所示:
而init()
方法的實現(xiàn)則是在HttpServletBean中,方法定義如下:
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
從實現(xiàn)上可以看出并沒有子容器相關代碼,但是它留了一個方法,用來讓子類擴展實現(xiàn)自己的初始化。而該方法的實現(xiàn)則是在FrameworkServlet中實現(xiàn)的,源碼如下:
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
而實際創(chuàng)建子容器的實現(xiàn)則是在initWebApplicationContext()
方法中實現(xiàn)的,該方法會創(chuàng)建子容器,并將先前創(chuàng)建的父容器從servletContext中取出來設置為子容器的父容器。
驗證
@Component
public class HelloService {
@Autowired
private ApplicationContext context;
public String sayHello(){
return "Hello World";
}
public ApplicationContext getContext(){
return context;
}
}
@RestController
@Slf4j
public class HelloWorldController {
@Autowired
private HelloService helloService;
@Autowired
private ApplicationContext context;
@GetMapping("hello")
public String helloWorld(){
//獲取Service中的容器
ApplicationContext parentContext = helloService.getContext();
//service中的容器并不等于controller中的容器
log.info("parentContext == context ? {}",parentContext == context);
//controller中的容器的父容器就是service中的容器
log.info("{}",parentContext == context.getParent());
return helloService.sayHello();
}
}
上面代碼中我們分別在HelloService和HelloWorldController中分別注入ApplicationContext,執(zhí)行程序最后的打印結果如下:
14:45:23.443 [http-nio-8080-exec-2] INFO c.b.c.c.HelloWorldController - parentContext == context ? false
14:45:23.451 [http-nio-8080-exec-2] INFO c.b.c.c.HelloWorldController - true
從上面的打印結果可以看出HelloService和HelloWorldController中的容器并不是同一個。
解決辦法
回到我們最初的問題,我們現(xiàn)在知道了AOP失效的原因是因為父子容器導致的,因為我們只在父容器中開啟了@AspectJ支持,在子容器中我們并沒有開啟。
只使用一個容器
既然問題是由父子容器導致的,那我們將controller層也交給父容器管理那是不是就可以了。實際上是沒有問題的,但是并不推薦這么做。
開啟子容器@AspectJ支持
在子容器的配置文件中增加如下配置:
<aop:aspectj-autoproxy/>
總結
對于Spring中父子容器的內容就講到這里了,后續(xù)如果還有新的發(fā)現(xiàn)會繼續(xù)更新相關內容。文中示例代碼地址:
https://github.com/I-Like-Pepsi/spring-example.git
本文由mdnice多平臺發(fā)布