结合源码浅谈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

<mvc:annotation-driven />

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

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

<!--配置注解控制器映射器,它是SpringMVC中用来将Request请求URL到映射到具体Controller-->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--配置注解控制器映射器,它是SpringMVC中用来将具体请求映射到具体方法-->
<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中配置了<context:component-scan base-package=“com.amu.modules" />

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

2.在applicationContext-MVC.xml中配置<mvc:annotation-driven />

负责SpringMVC相关注解的使用。

3.DEBUG 模式下启动项目,我们发现SpringMVC无法进行跳转,将log的日志打印发现SpringMVC容器中的请求没有映射到具体controller中。
4.在applicationContext-MVC.xml中配置<context:component-scan base-package=“com.amu.modules" />

重启后,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。

如下:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
   <property name="detectHandlerMethodsInAncestorContexts">
       <value>true</value>
   </property>
</bean>

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

配置方式如下

1.在applicationContext.xml中配置:

<!-- Spring容器中注册非@controller注解的Bean -->
<context:component-scan base-package="com.amu.modules">
   <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

2.applicationContext-MVC.xml中配置

<!-- SpringMVC容器中只注册带有@controller注解的Bean -->
<context:component-scan base-package="com.amu.modules" use-default-filters="false">
   <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

小结:

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

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

3.1 初探

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

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

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

<context:component-scan base-package="com.amu.modules.controller">   
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>   
</context:component-scan>  
2 扫描到其他注解

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

<context:component-scan base-package="com.amu.modules">   
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>   
</context:component-scan>  
这是因为什么呢?

3.2 源码分析

1.<context:component-scan>会交给org.springframework.context.config.ContextNamespaceHandler处理.

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

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

3.<context:component-scan>的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中进行如下配置:

<context:component-scan base-package="com.amu.modules"> 
     <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> 
</context:component-scan>

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

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

【公众号】:一只阿木木

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容