Spring中的父子容器
背景
在很長(zhǎng)的一段時(shí)間里面兔乞,關(guān)于Spring父子容器這個(gè)問(wèn)題我一直沒(méi)太關(guān)注招拙,但是上次同事碰見(jiàn)一個(gè)奇怪的bug于是我決定重新了解一下Spring中的父子容器夹抗。
項(xiàng)目是一個(gè)老的SSM項(xiàng)目糯彬,同事在使用AOP對(duì)Controller層的方法進(jìn)行攔截做token驗(yàn)證条霜。這個(gè)功能在實(shí)際的開(kāi)發(fā)項(xiàng)目中很常見(jiàn)對(duì)吧幸缕,估計(jì)大家都能輕易解決极舔。但是問(wèn)題就處在了AOP上面凤覆,根據(jù)AOP失效的八股文全部排查了一遍,問(wèn)題還是沒(méi)有解決拆魏。但是神奇的問(wèn)題出現(xiàn)了盯桦,我嘗試把切點(diǎn)放在Service中的方法AOP生效了。然后我看了下配置文件渤刃,發(fā)現(xiàn)了問(wè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");
}
}
其實(shí)問(wèn)題所在就是父子容器造成的,現(xiàn)在我們使用的SpringBoot中基本上不會(huì)出現(xiàn)問(wèn)題卖子,默認(rèn)情況下SpringBoot中只會(huì)有一個(gè)容器略号,而在傳統(tǒng)的SSM架構(gòu)中我們很可能會(huì)有兩個(gè)容器。在傳統(tǒng)的SSM架構(gòu)中洋闽,我們會(huì)創(chuàng)建兩個(gè)配置文件玄柠,一個(gè)用來(lái)創(chuàng)建Controller層的容器通常是子容器,而Service和Dao層的容器通常就是父容器诫舅。
父子容器相關(guān)接口
在IOC容器時(shí)羽利,Spring中通常會(huì)提到兩個(gè)頂級(jí)接口BeanFactory和ApplicationContext,這兩個(gè)都是IOC容器接口刊懈,相比BeanFactory而言这弧,ApplicationContext提供了更強(qiáng)大的功能。
HierarchicalBeanFactory
該接口作為BeanFactory的子接口俏讹,它的定義如下:
public interface HierarchicalBeanFactory extends BeanFactory {
BeanFactory getParentBeanFactory();
boolean containsLocalBean(String name);
}
從它名稱可以看出当宴,它是一個(gè)有層級(jí)的BeanFactory,它提供的兩個(gè)方法其中一個(gè)就是用來(lái)獲取父容器的泽疆。
ConfigurableBeanFactory
上面說(shuō)了HierarchicalBeanFactory提供了獲取父容器的方法户矢,那么父容器是怎么設(shè)置的呢?而設(shè)置父容器的方法則被定義在ConfigurableBeanFactory接口中殉疼。從名字可以看出它是一個(gè)可配置的BeanFactory梯浪,設(shè)置父容器的方法定義如下:
void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;
ApplicationContext
上面講了BeanFactory中獲取和設(shè)置父容器相關(guān)接口和方法,而ApplicationContext中同樣提供了一個(gè)方法用來(lái)獲取父容器瓢娜。
ApplicationContext getParent();
ConfigurableApplicationContext
與BeanFactory中設(shè)置父容器一樣挂洛,ConfigurableApplicationContext提供了一個(gè)用來(lái)設(shè)置父容器的方法。
void setParent(@Nullable ApplicationContext parent);
特性
通過(guò)上面介紹我們明白了什么是父子容器眠砾,那么它有哪些特性呢虏劲?使用時(shí)需要注意什么呢?
示例代碼如下:
- 父容器配置
@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無(wú)法獲取到父容器中的Bean
Map<String, ParentService> map = childContainer.getBeansOfType(ParentService.class);
map.forEach((k,v) -> log.info("{} => {}",k,v));
}
}
ParentService是父容器中的Bean,但是我們?cè)谧尤萜髦袇s能獲取到柒巫,這說(shuō)明在子容器中是可以獲取到父容器中的Bean的励堡,但是并不是所有方法都能,所以在使用時(shí)我們需要注意堡掏。這也解釋了一個(gè)問(wèn)題应结,那就是在SSM架構(gòu)中為什么我們能在Controller中獲取到Service,如果不是這個(gè)特性那我們的肯定是不行的泉唁。
父容器不能獲取子容器中的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());
}
}
}
上面的代碼運(yùn)行時(shí)會(huì)拋出異常亭畜,因?yàn)楦溉萜魇菬o(wú)法獲取到子容器中的Bean的扮休。
SSM中的父子容器
回到我們最初的問(wèn)題,在SSM中存在這兩個(gè)容器贱案,這也是導(dǎo)致我們前面AOP失敗的原因肛炮。那么SSM中的父子容器是如何被創(chuàng)建和設(shè)置的呢止吐?
web.xml
首先要解答這個(gè)問(wèn)題我們需要先來(lái)看一下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>
通常這個(gè)配置如上所示,我們需要關(guān)注的就兩分別為
ContextLoaderListener和DispatcherServlet碍扔。
父容器創(chuàng)建
其中ContextLoaderListener就是Servlet中的監(jiān)聽(tīng)器瘩燥,當(dāng)Servlet容器啟動(dòng)時(shí)就會(huì)調(diào)用contextInitialized()
方法進(jìn)行初始化,該方法的實(shí)現(xiàn)如下:
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
而initWebApplicationConte()
的實(shí)現(xiàn)則在ContextLoader這個(gè)類中不同,該方法的實(shí)現(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中(后面會(huì)用到)
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;
}
}
雖然方法較長(zhǎng)厉膀,但實(shí)際上我們需要關(guān)注的就三點(diǎn):
創(chuàng)建容器
配置并刷新容器
將容器設(shè)置到servletContext中
子容器創(chuàng)建
子容器的創(chuàng)建我們需要關(guān)注的就是web.xml中DispatcherServlet
配置了,DispatcherServlet
說(shuō)白了就是一個(gè)Servlet二拐,當(dāng)Servlet容器在實(shí)例化Servlet后就會(huì)調(diào)用其init()
方法就行初始化服鹅,而DispatcherServlet
的繼承如下圖所示:
而init()
方法的實(shí)現(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();
}
從實(shí)現(xiàn)上可以看出并沒(méi)有子容器相關(guān)代碼百新,但是它留了一個(gè)方法企软,用來(lái)讓子類擴(kuò)展實(shí)現(xiàn)自己的初始化。而該方法的實(shí)現(xiàn)則是在FrameworkServlet中實(shí)現(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");
}
}
而實(shí)際創(chuàng)建子容器的實(shí)現(xiàn)則是在initWebApplicationContext()
方法中實(shí)現(xiàn)的仗哨,該方法會(huì)創(chuàng)建子容器,并將先前創(chuàng)建的父容器從servletContext中取出來(lái)設(shè)置為子容器的父容器铅辞。
驗(yàn)證
@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í)行程序最后的打印結(jié)果如下:
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
從上面的打印結(jié)果可以看出HelloService和HelloWorldController中的容器并不是同一個(gè)。
解決辦法
回到我們最初的問(wèn)題斟珊,我們現(xiàn)在知道了AOP失效的原因是因?yàn)楦缸尤萜鲗?dǎo)致的苇倡,因?yàn)槲覀冎辉诟溉萜髦虚_(kāi)啟了@AspectJ支持,在子容器中我們并沒(méi)有開(kāi)啟。
只使用一個(gè)容器
既然問(wèn)題是由父子容器導(dǎo)致的旨椒,那我們將controller層也交給父容器管理那是不是就可以了胜嗓。實(shí)際上是沒(méi)有問(wèn)題的,但是并不推薦這么做钩乍。
開(kāi)啟子容器@AspectJ支持
在子容器的配置文件中增加如下配置:
<aop:aspectj-autoproxy/>
總結(jié)
對(duì)于Spring中父子容器的內(nèi)容就講到這里了辞州,后續(xù)如果還有新的發(fā)現(xiàn)會(huì)繼續(xù)更新相關(guān)內(nèi)容。文中示例代碼地址:
https://github.com/I-Like-Pepsi/spring-example.git
本文由mdnice多平臺(tái)發(fā)布