结合源码浅谈Spring容器与其子容器Spring MVC 冲突问题


容器是整个Spring 框架的核心思想,用来管理Bean的整个生命周期。

一个项目中引入Spring和SpringMVC这两个框架,Spring是父容器,SpringMVC是其子容器,子容器可以看见父容器中的注册的Bean,反之就不行。请记住这个特性。

spring 容器基础释义

1

我们可以使用统一的如下注解配置来对Bean进行批量注册,而不需要再给每个Bean单独使用xml的方式进行配置。

<context:component-scan base-package="com.amu.modules" />

该配置的功能是扫描配置的base-package包下的所有使用了@Component注解的类,并且将它们自动注册到容器中,同时也扫描其子类 @Controller,@Service,@Respository这三个注解

2

<context:annotation-config/>

此配置表示默认声明了@Required、@Autowired、 @PostConstruct、@PersistenceContext、@Resource、@PreDestroy等注解。

3


SpringMVC必备配置。它声明了@RequestMapping、@RequestBody、@ResponseBody等。并且,该配置默认加载很多的参数绑定方法,比如json转换解析器等。

4 上面的配置等价于spring3.1之后的版本:


<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>

二:案例分析

2.1 案例初探

Spring容器与其子容器Spring MVC 冲突问题的原因到底在那里?

我们知道,Spring和SpringMVC 容器配置文件分别为applicationContext.xml和applicationContext-MVC.xml。

1.在applicationContext.xml中配置了

负责所有需要注册的Bean的扫描和注册工作。

2.在applicationContext-MVC.xml中配置

负责SpringMVC相关注解的使用。

3.DEBUG 模式下启动项目,我们发现SpringMVC无法进行跳转,将log的日志打印发现SpringMVC容器中的请求没有映射到具体controller中。
4.在applicationContext-MVC.xml中配置

重启后,SpringMVC跳转有效。

2.2 查看源码

看源码SpringMVC的DispatcherServlet,当SpringMVC初始化时,会寻找SpringMVC容器中的所有使用了@Controller注解的Bean,来确定其是否是一个handler。

第1,2两步的配置使得当前springMVC容器中并没有注册带有@Controller注解的Bean,而是把所有带有@Controller注解的Bean都注册在Spring这个父容器中了,所以springMVC找不到处理器,不能进行跳转。(结合上文知识点理解)

核心源码如下:

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 (isHandler(getApplicationContext().getType(beanName))){
      detectHandlerMethods(beanName);
    }
  }
  handlerMethodsInitialized(getHandlerMethods());
}

在源码isHandler中会判断当前bean的注解是否是controller:

protected boolean isHandler(Class<?> beanType) {
  return AnnotationUtils.findAnnotation(beanType, Controller.class) != null;
}

在第4步配置中,SpringMVC容器中也注册了所有带有@Controller注解的Bean,故SpringMVC能找到处理器进行处理,从而正常跳转。

原因找到了,那么如何解决呢?

2.2 解决办法

在initHandlerMethods()方法中,detectHandlerMethodsInAncestorContexts这个Switch,它主要控制获取哪些容器中的bean以及是否包括父容器,默认是不包括的。

解决办法:在springMVC的配置文件中配置HandlerMapping的detectHandlerMethodsInAncestorContexts属性为true(根据具体项目看使用的是哪种HandlerMapping),让它检测父容器的bean。

如下:

class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
   
       true
   

我们按照官方推荐根据不同的业务模块来划分不同容器中注册不同类型的Bean:Spring父容器负责所有其他非@Controller注解的Bean的注册,而SpringMVC只负责@Controller注解的Bean的注册,使得他们各负其责、明确边界。

配置方式如下

1.在applicationContext.xml中配置:


package="com.amu.modules">
   

2.applicationContext-MVC.xml中配置


package="com.amu.modules" use-default-filters="false">
   

小结:

把不同类型的Bean分配到不同的容器中进行管理。

三:进阶:use-default-filters="false"的作用

3.1 初探

spring-mvc.xml 的不同配置方法

1 只扫描到带有@Controller注解的Bean

如下配置会成功扫描到带有@Controller注解的Bean,不会扫描带有@Service/@Repository注解的Bean,是正确的。

package="com.amu.modules.controller">   
        
  
2 扫描到其他注解

但是如下方式,不仅仅扫描到带有@Controller注解的Bean,还扫描到带有@Service/@Repository注解的Bean,可能造成事务不起作用等问题。

package="com.amu.modules">   
        
  
这是因为什么呢?

3.2 源码分析

1.会交给org.springframework.context.config.ContextNamespaceHandler处理.

registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); 

2.ComponentScanBeanDefinitionParser会读取配置文件信息并组装成org.springframework.context.annotation.ClassPathBeanDefinitionScanner进行处理。

3.的use-default-filters属性默认为true,在创建ClassPathBeanDefinitionScanner时会根据use-default-filters是否为true来调用如下代码:

protected void registerDefaultFilters() {
  this.includeFilters.add(new AnnotationTypeFilter(Component.class));
  ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
  try {
    this.includeFilters.add(new AnnotationTypeFilter(
      ((Class<? extends Annotation>) cl.loadClass("javax.annotation.ManagedBean")), false));
     logger.info("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
  }
  catch (ClassNotFoundException ex) {
    // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
  }
  try {
    this.includeFilters.add(new AnnotationTypeFilter(
      ((Class<? extends Annotation>) cl.loadClass("javax.inject.Named")), false));
    logger.info("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
  }
  catch (ClassNotFoundException ex) {
    // JSR-330 API not available - simply skip.
  }
}

从源码我们可以看出默认ClassPathBeanDefinitionScanner会自动注册对@Component、@ManagedBean、@Named注解的Bean进行扫描。

4.在进行扫描时会通过include-filter/exclude-filter来判断你的Bean类是否是合法的:

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
  for (TypeFilter tf : this.excludeFilters) {
    if (tf.match(metadataReader, this.metadataReaderFactory)) {
       return false;
    }
  }
  for (TypeFilter tf : this.includeFilters) {
    if (tf.match(metadataReader, this.metadataReaderFactory)) {
       AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
       if (!metadata.isAnnotated(Profile.class.getName())) {
          return true;
       }
       AnnotationAttributes profile = MetadataUtils.attributesFor(metadata, Profile.class);
       return this.environment.acceptsProfiles(profile.getStringArray("value"));
     }
   }
  return false;
}

从源码可看出:扫描时首先通过exclude-filter 进行黑名单过滤,然后通过include-filter 进行白名单过滤,否则默认排除。

3.3 结论

在spring-mvc.xml中进行如下配置:

package="com.amu.modules"> 
      

SpringMVC容器不仅仅扫描并注册带有@Controller注解的Bean,而且还扫描并注册了带有@Component的子注解@Service、@Reposity的Bean,从而造成新加载的bean覆盖了老的bean,但事务的AOP代理没有配置在spring-mvc.xml配置文件中,造成事务失效。因为use-default-filters默认为true。

结论:use-default-filters=“false”禁用掉默认。

【公众号】:一只阿木木