SpringMVC的URL映射器注册篇之BeanNameUrlHandlerMapping
写在前面
上一篇,我们讲解了 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”