用 Spring 管理 Controller,你覺(jué)得可行嗎?
上篇文章和小伙伴們聊了 Spring 容器中的父子容器問(wèn)題,也和小伙伴們梳理了 Spring 容器和 SpringMVC 容器之間的關(guān)系,其中,Spring 容器是父容器,SpringMVC 是子容器,父容器可以訪問(wèn)子容器中的 Bean,但是子容器無(wú)法訪問(wèn)父容器中的 Bean。
在一個(gè) SSM 項(xiàng)目中,你可以單純使用 SpringMVC 容器,這個(gè)沒(méi)問(wèn)題,項(xiàng)目可以正常運(yùn)行。但是,有的小伙伴可能要問(wèn)了,如果把所有的 Bean 都掃描到 Spring 容器中行不行?
先來(lái)說(shuō)結(jié)論:可以!但是需要額外配置。
閱讀本文需要先了解 Spring 容器的父子容器哦,如果還不了解的話建議先閱讀上篇文章。
為什么不能把所有 Bean 都注冊(cè)到 Spring 容器中呢?按照我們上篇文章中的分析,所有 Bean 都注冊(cè)到 Spring 容器之后,Spring 容器作為父容器,SpringMVC 作為子容器,按理說(shuō),由于子容器可以訪問(wèn)父容器中的 Bean,所以 SpringMVC 是可以正常訪問(wèn) Spring 容器中的 Bean 的,所以,似乎把所有的 Bean 都掃描到 Spring 容器應(yīng)該是沒(méi)有問(wèn)題的?
其實(shí)不然!
問(wèn)題就出在 SpringMVC 容器查找 Controller 的方式上,SpringMVC 容器查找 Controller,默認(rèn)情況下,只在當(dāng)前容器中查找,并不會(huì)去父容器中查找,所以如果把 Controller 都掃描到父容器的話,對(duì)于 SpringMVC 來(lái)說(shuō),相當(dāng)于系統(tǒng)中就沒(méi)有 Controller 了,所以你一訪問(wèn),直接就 404 了。
接下來(lái),我結(jié)合源碼和小伙伴們分析一下。
首先,小伙伴們知道,在 SpringMVC 中,當(dāng)請(qǐng)求到達(dá)服務(wù)端之后,需要由處理器映射器 HandlerMapping 來(lái)確定這個(gè)請(qǐng)求應(yīng)該由哪個(gè)處理器來(lái)處理,所以,按理說(shuō),HandlerMapping 中就會(huì)記錄所有的處理器信息,也就是 Controller 的信息。一般我們?cè)?SpringMVC 中使用的 HandlerMapping 都是 RequestMappingHandlerMapping,所以這里我們就通過(guò) RequestMappingHandlerMapping 的初始化來(lái)看一下,SpringMVC 到底是如何查找 Controller 的。
在 RequestMappingHandlerMapping#afterPropertiesSet 方法中,調(diào)用了父類的 afterPropertiesSet 方法,我們來(lái)看下:
AbstractHandlerMethodMapping#afterPropertiesSet:
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
initHandlerMethods 方法就是初始化處理器的方法,也就是在這個(gè)方法中,去嘗試找到所有的 Controller,并且把每一個(gè)接口方法都封裝成 HandlerMethod 對(duì)象。
我們來(lái)看下 getCandidateBeanNames 方法,這個(gè)方法用來(lái)找到所有的候選的 Bean:
protected String[] getCandidateBeanNames() {
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
}
關(guān)鍵點(diǎn)就在這了,這里首先去判斷 detectHandlerMethodsInAncestorContexts 變量的值,如果這個(gè)變量為 true,則調(diào)用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors 方法去查詢 Bean,這個(gè)方法在上篇文章中松哥和大家分享過(guò),用來(lái)查找 Bean 的名稱,包括父容器中的 Bean 都會(huì)查找到并返回;如果 detectHandlerMethodsInAncestorContexts 變量為 false,則調(diào)用 getBeanNamesForType 方法去查找 Bean,getBeanNamesForType 方法我們上篇文章也講過(guò),這個(gè)方法只找當(dāng)前容器的 Bean,不會(huì)去父容器中查找。
所以現(xiàn)在問(wèn)題的關(guān)鍵就在于 detectHandlerMethodsInAncestorContexts 變量了,這個(gè)變量默認(rèn)是 false,即,默認(rèn)情況下,只去當(dāng)前容器(SpringMVC 容器)查找 Bean。
這里找到的 beanName 是當(dāng)前容器中所有的 beanName,所以接下來(lái)還要去 processCandidateBean 方法走一圈,這個(gè)方法會(huì)去判斷這個(gè) Bean 是否是一個(gè) Controller,如果是就將之收集到一起:
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
beanType = obtainApplicationContext().getType(beanName);
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
可以看到,只有這類上有 @Controller 注解,這個(gè)類才會(huì)被留下來(lái)。
好啦,剩下的邏輯我們就不看了。
現(xiàn)在大家已經(jīng)了解到這樣一個(gè)情況:
SpringMVC 容器在初始化 HandlerMapping 的時(shí)候,會(huì)去查找所有的 Controller 并完成初始化,但是在默認(rèn)情況下,只會(huì)去當(dāng)前容器中查找,并不會(huì)去父容器中查找。
所以,如果把 Controller 讓 Spring 容器掃描并管理,那么就會(huì)導(dǎo)致在默認(rèn)情況下,SpringMVC 容器找不到 Controller,進(jìn)而導(dǎo)致所有的請(qǐng)求 404。
在前面的講解中,松哥都強(qiáng)調(diào)了默認(rèn)情況,意思就是說(shuō)這個(gè)事情還有轉(zhuǎn)圜的余地,看了前面源碼的小伙伴應(yīng)該也發(fā)現(xiàn)了,只要我們把 detectHandlerMethodsInAncestorContexts 變量改為 true,那么 HandlerMapping 就會(huì)去父容器中查找 Bean,這樣即使被 Spring 容器掃描并管理的 Bean,也就能夠查找到了。
修改方式如下:
spring-servlet.xml:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="detectHandlerMethodsInAncestorContexts" value="true"/>
</bean>
在 Spring 容器中直接掃描所有 Bean:
<context:component-scan base-package="org.javaboy.web"/>
web.xml 中加載這兩個(gè)配置文件:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
這樣配置之后,就可以把所有 Bean 都掃描到 Spring 容器中了。
好啦,今天這篇文章目的不是為了讓小伙伴們?nèi)ピ?Spring 容器中管理 Controller,只是想借這樣一個(gè)契機(jī),一起來(lái)捋一捋 SpringMVC 中 HanderMapping 的原理。