大家好,我是磊叔的豬弟,豬在我心中從來不是蠢的代名詞,而是懶的代名詞,本次準備記錄一個在開發測試過程中遇到的問題,跟蹤了三天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.xml
和spring-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為了保證注入類的一致性,採用了雙親委託的機制,如果父容器中存在該類的例項那麼優先使用父容器中的例項,如果父容器中沒有該例項才會用子容器中的例項