1. 程式人生 > >結合原始碼淺談Spring容器與其子容器Spring MVC 衝突問題

結合原始碼淺談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”禁用掉預設。

【公眾號】:一隻阿木木