寫在前面
上一篇,我們講解了 SpringMVC的URL映射器之SimpleUrlHandlerMapping,列舉了常見的配置方法,並且分析了主要源碼。這一篇我們來分析另一個 URL 映射器。
概述 BeanNameUrlHandlerMapping
上圖是 BeanNameUrlHandlerMappping 的繼承層次圖。
-
HandlerMapping 是通用接口,包含一個 getHandler(req:HttpServletRequest):HandlerExecutionChain 方法,該方法用來尋找請求對應的處理器鏈
-
AbstractHandlerMapping,用 interceptors 保存攔截器,並負責選取攔截器並加入到 HandlerExecutionChain 當中
-
AbstractUrlHandlerMapping,用 handlerMap 保存 url 和 “Handler” 之間的映射
-
AbstractDetectingUrlHandlerMapping,從 Spring 容器中檢出 URL 映射
-
BeanNameUrlHandlerMapping,篩選出名稱以“/”開頭的 Bean,並把這些 Bean 的名稱組成一個列表。
使用 BeanNameUrlHandlerMapping 的方式也十分簡單,延續了上一篇文章的代碼,只是替換了 spring-mvc.xml 中的內容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Fixed problem : javax.servlet.ServletException: No adapter for handler [coderead.springframework.mvc.LoginHttpServlet@2f2f30df]-->
<bean class="org.springframework.web.servlet.handler.SimpleServletHandlerAdapter"/>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" />
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
<!--注入 BeanNameUrlHandlerMapping Bean-->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
<!--使用 name 可以創建一個或者多個在 id 標簽中“非法”的別名-->
<bean name="/welcome" class="coderead.springframework.mvc.WelcomeController" />
<bean name="/hello" class="coderead.springframework.mvc.HelloGuestController" />
<bean name="/hi" class="coderead.springframework.mvc.HelloLuBanHttpRequestHandler" />
<bean name="/login" class="coderead.springframework.mvc.LoginHttpServlet" />
</beans>
源碼解析
BeanNameUrlHandlerMapping
BeanNameUrlHandlerMapping # determineUrlsForHandler 點擊展開看源碼
protected String[] determineUrlsForHandler(String beanName) {
List
urls = new ArrayList<>(); // bean 的名稱以 “/” 開頭 if (beanName.startsWith("/")) { urls.add(beanName); } String[] aliases = obtainApplicationContext().getAliases(beanName); for (String alias : aliases) { // bean 的別名以 “/” 開頭 if (alias.startsWith("/")) { urls.add(alias); } } return StringUtils.toStringArray(urls); }
看完這段源碼,我就在想:什么情況下,是通過 beanName 判斷的,什么時候是根據 beanName 的別名判斷的?
第一種:單獨指定 id 或者 name
<bean id="/welcome" class="coderead.springframework.mvc.WelcomeController" />
<bean name="/hello" class="coderead.springframework.mvc.HelloGuestController" />
此時,容器把 <bean> 唯一的 name 或者 id 作為 beanName。
第二種:即指定 id 又指定 name
<bean id="welcomeController" name="/welcome" class="coderead.springframework.mvc.WelcomeController" />
此時,容器把 "welcomeController" 作為 beanName,把 "/welcome" 作為別名,就會走到下面的循環中。
綜上所述,生產中我更推薦第二種配置方式,這樣更符合一般習慣。
AbstractDetectingUrlHandlerMapping
AbstractDetectingUrlHandlerMapping # detectHandlers 點擊展開看源碼
protected void detectHandlers() throws BeansException {
// 得到應用程序上下文,該上下文是由 Spring 容器通過 {@link ApplicationContextAware} 調用以注入當前應用程序的
ApplicationContext applicationContext = obtainApplicationContext();
// detectHandlersInAncestorContexts 默認是 false
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
applicationContext.getBeanNamesForType(Object.class));
// 獲取所有的 beanName 用來決定 URLs
for (String beanName : beanNames) {
// 由子類來決定 Handler 的 url 集合
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// URL paths found: Let's consider it a handler.
registerHandler(urls, beanName);
}
}
if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) {
logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName());
}
}
detectHandlersInAncestorContexts 默認是 false,如果你設置為 true,你就可以從 Root WebApplicationContext 中獲取映射了。接下來就教大家讓 detectHandlersInAncestorContexts = true 生效的配置方法:
首先,你的 web.xml 需要包含以下內容
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
為了順利啟動,你可能還需要一個創建文件 WEB-INF/applicationContext.xml 和 web.xml 放在同一目錄下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="welcomeController" name="/welcome" class="coderead.springframework.mvc.WelcomeController" />
</beans>
最后,你還需要改一下放在 resources 下的 spring-mvc.xml,為 BeanNameUrlHandlerMapping 修改 detectHandlersInAncestorContexts 屬性。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--注入 BeanNameUrlHandlerMapping Bean-->
<bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="detectHandlersInAncestorContexts" value="true" />
</bean>
</beans>
現在我們就可以把 “根應用程序上下文” 中注冊的 Bean 作為 value,URL 作為 key,通過調用 AbstractUrlHandlerMapping # registerHandler 放入到 handlerMap 中去了。
結語
攔截器不是本文的重點,所以 AbstractHandlerMapping 就不放在本文講解了。
BeanNameUrlHandlerMapping 和 SimpleUrlHandlerMapping 同屬於 AbstractUrlHandlerMapping 的子類,他們都有 URL 映射處理器的能力。
BeanNameUrlHandlerMapping 篩選出 Name 或者 別名以 "/" 開頭的 Bean ,將這些 Bean 注冊為 “Handler”,實現 URL 映射。這種方式在配置上會比 SimpleUrlHandlerMapping 要便利一些。
我這里給處理器 “Handler” 打上雙引號,是因為這些處理器並沒有統一的 Handler 接口,而是通過適配器進行轉換的。所以概念上認為是統一的 “Handler”,但是從語法和類繼承結構上,又算不上統一的 “Handler”