關於 Spring 父子容器的三個問題
前言
對 Spring 父容器和子容器做了一個案例的測試。對於已有的問題進行了一個好的測試。
正文
我先把本項目的Web啓動類,以及一些基本配置發上來。關於如何構建一個Web項目,可以參數我的這篇文章《純Java啓動Web(無配置web.xml)》。
WebApp.java(啓動類)
package vip.wulang.start;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import vip.wulang.config.ChildContext;
import vip.wulang.config.ParentContext;
/**
* @author CoolerWu on 2018/11/18.
* @version 1.0
*/
public class WebApp extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ParentContext.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{ChildContext.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
ParentContext.java(父容器也就是 Spring 容器)
package vip.wulang.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* @author CoolerWu on 2018/11/18.
* @version 1.0
*/
@Configuration
@ComponentScan(
value = "vip.wulang",
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = EnableWebMvc.class)
)
public class ParentContext implements ApplicationContextAware {
private static ApplicationContext parentContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.parentContext = applicationContext;
}
public static ApplicationContext getParentContext() {
return parentContext;
}
}
ChildContext.java(子容器也就是 Spring MVC 容器)
package vip.wulang.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import vip.wulang.controller.UserController;
/**
* @author CoolerWu on 2018/11/18.
* @version 1.0
*/
@Configuration
@EnableWebMvc
@ComponentScan("vip.wulang.controller")
public class ChildContext extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private static ApplicationContext childContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.childContext = applicationContext;
}
public static ApplicationContext getChildContext() {
return childContext;
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/view/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
UserController.java(Controller類,用於測試)
package vip.wulang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import static vip.wulang.config.ParentContext.getParentContext;
import static vip.wulang.config.ChildContext.getChildContext;
@Controller
public class UserController {
}
問題一:Spring 父容器和子容器掃描同一個類時,子容器還會創建一個新的bean實例嗎?
其實這個掃描跟在 Java 配置類裏面寫一個方法並帶有 @Bean 註解是一個道理,倘若 ParentContext 和 ChildContext 都有一個 bean 實例,那肯定不一樣。我們使用 @ComponentScan 註解來掃描同一個包,來看看具體結果,現在 UserController 類添加如下代碼:
@RequestMapping("/")
@ResponseBody
public String getAllUser() {
System.out.println(getParentContext()); // 父容器
System.out.println(getChildContext()); // 子容器
System.out.println(getChildContext().getParent() == getParentContext()); // 驗證是否屬於父子關係
System.out.println(getParentContext().getBean("userController") == getChildContext().getBean("userController")); // 驗證兩個容器的 userController 是否不同
return "ok";
}
Root WebApplicationContext: startup date [Sun Nov 18 15:24:57 CST 2018]; root of context hierarchy
WebApplicationContext for namespace ‘dispatcher-servlet’: startup date [Sun Nov 18 15:24:57 CST 2018]; parent: Root WebApplicationContext
true
false
看見結果,就知道了。總結:父容器和子容器同時掃描一個類會產生不同實例,並且倘若子容器沒有該實例,可以從父容器裏面獲取。
問題二:一個類被父容器和子容器同時掃描,並且它也實現了 ApplicationContextAware 這個接口,那麼注入的是哪個接口呢,或者說會報錯呢?
話不多說,進行測試,我還是在原來的基礎上修改了 UserController 一個類而已,代碼如下:
package vip.wulang.controller;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import static vip.wulang.config.ParentContext.getParentContext;
import static vip.wulang.config.ChildContext.getChildContext;
@Controller
public class UserController implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@RequestMapping("/")
@ResponseBody
public String getAllUser() {
System.out.println(applicationContext == getParentContext()); // 是否是父容器
System.out.println(applicationContext == getChildContext()); // 是否是子容器
return "ok";
}
}
false
true
你以爲就是子容器注入成功了?是的沒錯,它確實是注入成功了,但是父容器也是被注入過,只不過子容器是最後被注入的。稍微修改 UserController 類的方法 setApplicationContext 代碼:
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
// 如果是父容器,輸出
if (applicationContext == getParentContext()) {
System.out.println("Parent is coming...");
}
// 如果是子容器,輸出
if (applicationContext == getChildContext()) {
System.out.println("Child is coming...");
}
}
在控制檯上尋找,你會發現:
Parent is coming…
Child is coming…
原來 Spring 注入 ApplicationContext 類,是按照一定順序注入的,先注入父容器,再注入子容器,最後注入的才能決定真正的 ApplicationContext 的引用類型。
問題三:Spring與SpringMVC的容器衝突的原因到底在那裏?
我們先把 ChildContext 類的 @ComponentScan(“vip.wulang.controller”) 註釋掉,啓動服務器,會出現 404,表示沒有找到。那我們先把 ChildContext 類的 @ComponentScan(“vip.wulang.controller”) 的註釋去掉,然後把 ParentContext 類的 @ComponentScan 註解中掃描到 vip.wulang.controller 這個包給排除掉,如下:
// ParentContext 類
@ComponentScan(
value = "vip.wulang",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = EnableWebMvc.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
}
)
啓動服務器,我們會發現該服務器可以正常運作並且返回了JSON “ok”,通過Debug,我們找到了 AbstractHandlerMethodMapping 類,該類的 initHandlerMethods() 方法的作用是掃描ApplicationContext中的bean,檢測和註冊處理程序方法,該方法的源代碼如下:
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
try {
beanType = getApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
// 見下面 RequestMappingHandlerMapping 類的部分源代碼
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
RequestMappingHandlerMapping 類的 isHandler() 方法的作用是判斷是否含有 Controller 或者 RequestMapping 註解,該方法的源代碼如下:
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
我們會發現,Spring MVC 容器只在該容器中查找 bean 實例,而沒有去查找父容器,這就是關鍵所在。根據官方建議我們就可以很好把不同類型的Bean分配到不同的容器中進行管理。所以沒必要讓父容器也掃描 @Controller 註解,可以進行剔除。
結束語
遇到每一個知識點,都應該自己去發散思維,把自己不懂得都給用代碼測試出來,只有親眼所見才能印象深刻。