【SpringMVC】<context:include-filter>&&<context:exclude-filter>爬坑

glmapper_2018發表於2018-01-20

  大家好,我是磊叔的豬弟,豬在我心中從來不是蠢的代名詞,而是懶的代名詞,本次準備記錄一個在開發測試過程中遇到的問題,跟蹤了三天spring和第三方RPC元件的原始碼,最終發現了問題是因為第三方元件沒有處理好而父子容器導致的,還有一個因素是spring註解掃描重疊。

Spring版本:4.3.13.RELEASE

IDE工具:IDEA 2017.2.6

JDK版本:1.7_u25 64位

SpringMVC的配置中為了防止Spring重複建立同一個類的例項,一般會用到<context:component-scan>的兩個子標籤<context:include-filter>&&<context:exclude-filter>

但它使用的時候表現的效果並不是和語義上的完全一致,現在來看一下其中的坑:

在很多配置中一般都會把spring-config.xmlspring-mvc.xml進行分開配置,這種配置可以他們保證各司其職,在web.xml的一般配置中spring-mvc.xml例項建立初始化是以DispatchServlet為入口,而spring-config.xml例項建立初始化是以ContextLoadListener為入口的,容器的載入順序:listener -> filter -> servlet ,所以spring容器先初始化,springmvc容器後初始化 。

    <!--spring 入口-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring-config.xml
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!--spring mvc 入口-->
    <servlet>
        <servlet-name>blog-spring-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                classpath:spring-mvc.xml
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>blog-spring-mvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
複製程式碼

如果在spring-mvc.xml中配置掃描的包和spring-config.xml中的發生重疊,那麼會導致一個bean被建立兩次,而且在spring中是存在父子容器的,spring容器是父容器,springmvc是子容器,springmvc建立的例項放在子容器中,spring建立的例項放在父容器中。

其實這同一個類的兩個例項是不同的,springmvc建立例項預設物件不實現介面(大家都知道Controller是不用實現介面的),所以springmvc建立的例項是直接使用目標類的構造器來例項化的,而不是代理物件,即使一個類實現了介面,但如果該類是由springmvc例項化,那麼springmvc也會直接使用該類的構造器直接建立一個物件(怎麼去證明呢,你可以寫一個定時任務,在定時任務中注入Controller的例項,然後debug檢視例項物件的地址,如果是代理物件在地址上都會有一個$Proxy的標記,否則就不是代理物件),所以在controller層使用AOP時多數採用的是CGLIB子類代理。

Spring建立例項會判斷目標類是否實現了介面,如果沒實現介面那麼就直接採用目標類構造器建立,像一般的service和dao都會採用介面方式程式設計,對於介面方式程式設計的類,spring建立的例項都是代理物件(這一點可以用debug的方式檢視controller類中注入的service例項物件地址,他們都帶有一個$Proxy的標記,很容易就能看出都是代理物件)。

那麼為了防止重疊我們要把重疊的部分去掉,現在有下面的一個需求:

spring-mvc.xml中只對工程中所有用@Controller註解的類進行掃描建立例項。

spring-config.xml中要對工程中所有的非@Controller註解的類進行掃描建立例項。

現在給定一個專案的包結構:

xin.sun.blog.controlller

xin.sun.blog.service

(1)在spring-mvc.xml中有以下配置:

<!-- 只掃描 @Controller註解-->
<context:component-scanbase-package="xin.sun.blog.controlller">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
複製程式碼

可以看出要把最後的包寫上,不能包含子包,所以不能寫成: base-package="xin.sun.blog" 。如果這樣寫,對於 include-filter 標籤來講它會掃描基包下面所有spring註解的類,而不是僅僅掃描 @Controller 。這點需要非常的注意,這一般會導致一個常見的錯誤,那就是事務不起作用,補救的方法是新增 use-default-filters="false"

(2)在spring-config.xml中有如下配置:

<!-- 配置掃描註解,不掃描 @Controller註解-->
<context:component-scan base-package="xin.sun.blog">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
複製程式碼

可以看到,他是要掃描xin.sun.blog包和子包下的所有spring註解的類,但是不包含@Controller註解的類。對於exculude-filter不存在包不精確導致都進行掃描的問題。

那麼還有一個問題:當掃描的包不小心重疊了,導致類在父子容器各例項化了一遍,在 @Autowire 的時候會注入哪個容器中的物件呢?看一個Controller類,程式碼如下:

@Controller
public class MyController{

    @Autowired
    private IValidService validService;
    //其他程式碼省略 
}
複製程式碼

答案是:Spring為了保證注入類的一致性,採用了雙親委託的機制,如果父容器中存在該類的例項那麼優先使用父容器中的例項,如果父容器中沒有該例項才會用子容器中的例項

相關文章