关于 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 注解,可以进行剔除。
结束语
遇到每一个知识点,都应该自己去发散思维,把自己不懂得都给用代码测试出来,只有亲眼所见才能印象深刻。