HandlerMapping帮助DispathcerServlet进行Web请求的URL到具体处理类的匹配。
之所以被称为HandlerMapping是因为,在Spring MVC中,并不只局限于使用org.springframework.web.servlet.mvc.Controller作为DispatcherSevlet的次级控制器来进行具体的Web请求的处理。
实际上,在稍后介绍HandlerAdaptor的时候,就会知道,我们也可以使用其他类型的次级控制器,包括Spring MVC提供的除了Controller之外的次级控制器类型,或者第三方Web开发框架中的Page controller组件,而所有这些次级控制器类型,在Spring MVC中都被称为Handler。
因此HandlerMapping要处理的其实就是Web请求到相应Handler之间的映射关系。
HandlerMapping的定义如下:
public interface HandlerMapping {
String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";
@Deprecated
String LOOKUP_PATH = HandlerMapping.class.getName() + ".lookupPath";
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
default boolean usesPathPatterns() {
return false;
}
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
HandlerMapping支持请求映射过程中添加对应的拦截器,这就是我们熟悉的并经常在Spring mvc中使用的拦截器链,这也就是解释了,为什么getHandler方法返回的是一个HandlerExecutionChain,因为该HandlerExecutionChain中会包含可以应用到当前handler上的一堆拦截器还有handler自身。
当DispathcerServlet要调用该handler时,会先去调用拦截器链中每个拦截器的preHandler方法,如果都返回true,再去调用对应的hanler。
tomcat在调用某个servlet之前,也会去调用该servlet相关的过滤器链,然后最终再去调用servlet的service方法 对于spring mvc的DispathcerServlet来说,要先等到tomcat的相关过滤器链执行完毕,再去执行DispathcherServlet,然后DispathcerServlet再将请求分发给具体Handler之前,还需要先调用当前handler关联的一堆拦截器
Spring MVC默认提供了多个HandlerMapping的实现:
我们重点关注下面几个实现类:
BeanNameUrlHandlerMapping类的定义如下:
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
//当前handler可以映射到哪些URLS上去
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<>();
//如果当前Handler的beanName以/开头,那么对应handler就可以映射到该请求上
if (beanName.startsWith("/")) {
urls.add(beanName);
}
//获取当前bean的别名数组
String[] aliases = obtainApplicationContext().getAliases(beanName);
//如果别名中也有/开头的,那么也加入URLS集合
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
//返回的是当前handler可以处理的URLS集合
return StringUtils.toStringArray(urls);
}
}
这里不需要去看看其父类中的实现,只需要根据BeanNameUrlHandlerMapping实现的determineUrlsForHandler方法即可推断出,该HandlerMapping的具体映射规则。
所以,如果要使用BeanNameUrlHandlerMapping,只需要在对应[servletName]-servlet.xml配置文件中,注入该bean即可: (当然,不注入的话,也会默认使用BeanNameUrlHandlerMapping)
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<bean name="/hhh.do" class="com.example.controller.HelloController">
<property name="helloService" ref="helloService"/>
<property name="viewName" value="hello"/>
</bean>
使用BeanNameUrlHandlerMapping进行Web请求到具体Handler的映射管理,需要我们保证视图模板中的请求路径,必须与容器中对应的Handler的beanName一致。
BeanNameUrlHandlerMapping的固定映射模式,并没有对映射做个过程做过多的关注,而是简单的直接匹配。
SimpleUrlHandlerMapping比BeanNameUrlHandlerMapping做的工作多一些,它可以使视图一方和handler一方自由活动,最后由SimpleUrlHandlerMapping进行统筹。
<?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="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/hello.do">helloController</prop>
</props>
</property>
</bean>
<bean name="helloController" class="com.example.controller.HelloController">
<property name="helloService" ref="helloService"/>
<property name="viewName" value="hello"/>
</bean>
</beans>
这里所有请求后面要加.do是因为在web.xml中配置的DispatcherServlet拦截的路径是 * .do
现在控制器Controller可以起任何名字,视图中的链接也可以独立提供,只需要通过SimpleUrlHandlerMapping指定一下二者的对应关系即可。
SimpleUrlHandlerMapping 的源码也非常的简单:
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
//存放controller控制器和其对应映射到的请求路径
private final Map<String, Object> urlMap = new LinkedHashMap<>();
//下面两个方法对应xml中往urlMap中注入属性的两种不同方式
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
public void setUrlMap(Map<String, ?> urlMap) {
this.urlMap.putAll(urlMap);
}
public Map<String, ?> getUrlMap() {
return this.urlMap;
}
= //该方法会在父类中被调用,因为父类实现了ApplicationContextAware接口
//初始化时,会去调用setApplicationContext方法,其中会调用 initApplicationContext方法
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
//注册好handler的相关映射
registerHandlers(this.urlMap);
}
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
}
else {
//这里注意是处理配置文件中,给出请求路径时,没加/的情况,和空格处理
urlMap.forEach((url, handler) -> {
// Prepend with slash if not already present.
if (!url.startsWith("/")) {
url = "/" + url;
}
// Remove whitespace from handler bean name.
if (handler instanceof String) {
handler = ((String) handler).trim();
}
//调用父类方法进行注册---注册到父类的handlerMap映射集合中去
registerHandler(url, handler);
});
}
}
}
SimpleUrlHandlerMapping还可以使用类似于ANT路径形式的模式匹配,这样我们就可以通过各种表达式,将一组或多组拥有某种相似特征的Web处理请求映射给相应的Handler处理。
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/**/*hello.do">helloController</prop>
</props>
</property>
</bean>
<bean name="helloController" class="com.example.controller.HelloController">
<property name="helloService" ref="helloService"/>
<property name="viewName" value="hello"/>
</bean>
当然,其实使用BeanNameUrlMapping也可以使用ANT路径匹配,只不过这里要使用ANT路径的地方,是对应控制器的beanName
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<bean name="/h*h.do" class="com.example.controller.HelloController">
<property name="helloService" ref="helloService"/>
<property name="viewName" value="hello"/>
</bean>
为什么,可以玩呢?
@Nullable
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
// Direct match?
//直接匹配--去handlerMap中,直接根据urlPath去进行精确查找
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
//如果精确查找到了,那么判断此时handlerMap中找到的这个handler是不是一个字符串
//为什么会是一个字符串呢?
//因为,如果该handler是一个多例类型,那么每次都需要new一个新的实例,因此handlerMap保存的是该handler的beanName
//每次通过getBean重新创建该handler
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
//以当前handler构建一个空的HandlerExecutionChain,暂时还未加入相关拦截器
return buildPathExposingHandler(handler, urlPath, urlPath, null);
}
//采用ant模式进行路径匹配
// Pattern match?
List<String> matchingPatterns = new ArrayList<>();
for (String registeredPattern : this.handlerMap.keySet()) {
//getPathMatcher返回的就是AntPathMatcher---对注册好的请求路径与当前请求路径进行ant匹配
if (getPathMatcher().match(registeredPattern, urlPath)) {
//如果满足条件,则加入集合
matchingPatterns.add(registeredPattern);
}
//useTrailingSlashMatch默认为false
//即不会把"/users"当做"/users/"
else if (useTrailingSlashMatch()) {
//如果注册的映射路径不以/结尾,这里加上/,再进行ant匹配,判断是否满足
//such as "/users" also matches to "/users/"
if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
matchingPatterns.add(registeredPattern + "/");
}
}
}
//如果上面ant匹配存在多个matchingPattern,那么下面会挑选出一个最佳匹配
//遵循的是精确优先
String bestMatch = null;
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
if (!matchingPatterns.isEmpty()) {
matchingPatterns.sort(patternComparator);
if (logger.isDebugEnabled()) {
logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);
}
bestMatch = matchingPatterns.get(0);
}
//判断ant匹配是否可以得到一个结果
if (bestMatch != null) {
//如果得到了,那么拿到对应的handler
handler = this.handlerMap.get(bestMatch);
//如果handler为空,那么尝试取出掉bestMatch最后的/,再次进行查找匹配
if (handler == null) {
if (bestMatch.endsWith("/")) {
handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
}
//如果还是没找到,则抛出异常
if (handler == null) {
throw new IllegalStateException(
"Could not find handler for best pattern match [" + bestMatch + "]");
}
}
// Bean name or resolved handler?
//处理多例的情况
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
// There might be multiple 'best patterns', let's make sure we have the correct URI template variables
// for all of them
Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
for (String matchingPattern : matchingPatterns) {
if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
uriTemplateVariables.putAll(decodedVars);
}
}
if (logger.isDebugEnabled()) {
logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);
}
return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
}
// No handler found...
return null;
}
先精确查找,再模糊匹配,如果存在多个匹配结果,那么精确优先。
在基于Spirng MVC的Web应用程序中,我们可以为DispathcerServlet提供多个HandlerMapping,DispatcherServlet在选用HandlerMapping的过程中,会先对HandlerMapping集合按照优先级进行排序,然后按照依次询问每个HandlerMapping。
initHandlerMappings方法中会对HandlerMappings集合进行排序,并且该方法是在onRefresh方法中被调用的
如果某一个HandlerMapping能够返回可用的Handler,则DispathcerServlet使用当前Handler进行Web请求的处理,而不再询问其他HandlerMapping。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//handlerMappings集合中存放的是当前捕获到可用的handlerMapping集合
if (this.handlerMappings != null) {
//按照优先级顺序依次遍历
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace(
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
//如果某个handlerMapping返回了值,那么直接退出循环
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
HandlerMapping的优先级规定遵循了Spring框架内一贯的Oredered接口所规定的语义,数值越小,优先级越大,具体配置实例如下:
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="order" value="1"/>
</bean>
Controller是Spring MVC框架支持的用于处理具体Web请求的Handler类型之一。
要实现一个具体的Controller,我们可以直接实现Controller接口:
@FunctionalInterface
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
直接实现Controller接口当然没有问题,但是这需要我们去处理很多的通用细节问题,例如: 请求参数的抽取,请求编码的设定,国际化信息处理,Session数据的管理等。
而实际上,这些关注点是所有Controller都需要的,我们就应该想办法将这些通用的逻辑进行复用,这就是Spring MVC提供了一套Controller实现体系的原因。它帮助我们更好地处理了Web请求过程中的某些通用关注点。
Controller的继承体系如下:
Spring 4开始,对原有的Controller架构进行了大调整,大家上面看到的这些类基本都被移除了,Spring 4开始的Controller架构如下,我们讲3,再看4
AbstractControlle是整个Controller继承体系的起源,该类通过模板方法模式帮助我们解决了如下几个通用关注点:
我们要做的,就是在AbstractController所公开的handleRequestInternal模板方法中实现具体Wbe请求处理过程中的其他逻辑。
DispatcherServlet会去调用AbstractController实现Controller接口的handleReqeuest方法,而AbstractController为了做一些准备工作,因此需要在handleReqeuest提前做好,然后将真正请求处理过程再通过抽象方法handleRequestInternal暴露给子类去实现。
handleRequest是AbstractController模板方法模式的体现
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
throws Exception {
// Delegate to WebContentGenerator for checking and preparing.
//做上面讲的通用关注点处理--由父类WebContentGenerator完成的
checkAndPrepare(request, response, this instanceof LastModified);
// Execute handleRequestInternal in synchronized block if required.
//是否需要开启session同步锁机制
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
return handleRequestInternal(request, response);
}
}
}
//进入真正处理请求的方法逻辑
return handleRequestInternal(request, response);
}
protected final void checkAndPrepare(
HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
throws ServletException {
// Check whether we should support the request method.
//是否支持当前请求方法
String method = request.getMethod();
if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
throw new HttpRequestMethodNotSupportedException(
method, StringUtils.toStringArray(this.supportedMethods));
}
// Check whether a session is required.
//是否需要session
if (this.requireSession) {
if (request.getSession(false) == null) {
throw new HttpSessionRequiredException("Pre-existing session required but none found");
}
}
// Do declarative cache control.
// Revalidate if the controller supports last-modified.
//是否需要设置缓存相关响应头
applyCacheSeconds(response, cacheSeconds, lastModified);
}
可以看到AbstractController并没有做很多事情,因此如果我们实现AbstractController来完成业务逻辑编写,会发现还是需要关注参数抽取,数据验证等细节。
该类在Spring 4中已经被移除了
MultiActionController用于对一组逻辑上相近的Web请求进行封装,例如针对同一个对象的CRUD进行封装。
而不需要像AbstaractController那样为每一个请求单独实现一个继承AbstaractController的处理类。
MultiActionController提供了以下功能:
为了在MultiActionController中处理多个Web请求,我们需要定义多个Web请求处理方法,分别对应每个Web请求的处理。
这些Web请求处理方法可以定义在MultiActionController的子类中,也可以定义在某一个将来可以指定给MultiActionController的委派对象(delegate)内,但Web请求的处理方法的签名必须符合一定的要求:
public (ModelAndView | Map | String | void) actionName
(HttpServletRequest request, HttpServletResponse response, [,HttpSession] [,AnyObject]);
Web请求处理方法的名称可以取任何有意义的名字,但是前面两个方法参数是必须的,第三个参数是可选的,可以是HttpSession类型也可以是Object类型,如果是Object类型,则表明对应的是Command对象,那么MultiActionController就会帮我们绑定数据并执行数据验证了。
最后一个参数可以是我们自定义的对象,只需要给其中对应的属性提供get和set方法,MultiActionController便会在参数绑定时,去请求参数中尝试将同名属性绑定到对应的对象属性上面去
方法的返回值有三种类型,分别对应如下语义:
现在还有一个问题: MultiActionController如何知道将当前请求映射到哪个方法上呢?
MethodNameResolver决定当前Web请求交给哪个方法来处理,其定义如下:
public interface MethodNameResolver {
String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException;
}
有了MethodNameResolver 之后,当请求到来时,MultiActionController只需要询问一下MethodNameResolver 需要调用哪个方法即可:
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
try {
String methodName = this.methodNameResolver.getHandlerMethodName(request);
return invokeNamedMethod(methodName, request, response);
}
catch (NoSuchRequestHandlingMethodException ex) {
return handleNoSuchRequestHandlingMethod(ex, request, response);
}
}
MethodNameResolver 为MultiActionController提供了灵活的Web请求到对应处理方法的映射策略,包括根据Web请求的URL进行映射,或者根据某个参数值进行映射等。
我们下面来看看Spring为我们提供好了哪些MethodNameResolver 实现:
MethodNameResolver本身是一个策略接口,Spring默认提供了三个实现类:
AbstractUrlMethodNameResolver是基于请求URL到具体处理方法的抽象解析器父类,该类中有两个重要的方法:
//根据请求获得对应handlerMethod的方法名
public final String getHandlerMethodName(HttpServletRequest request)
throws NoSuchRequestHandlingMethodException {
//查询出当前请求对应的URL路径
String urlPath = this.urlPathHelper.getLookupPathForRequest(request);
//调用子类实现的抽象方法,来获取到URL具体映射到的方法名
String name = getHandlerMethodNameForUrlPath(urlPath);
if (name == null) {
throw new NoSuchRequestHandlingMethodException(urlPath, request.getMethod(), request.getParameterMap());
}
if (logger.isDebugEnabled()) {
logger.debug("Returning handler method name '" + name + "' for lookup path: " + urlPath);
}
return name;
}
protected abstract String getHandlerMethodNameForUrlPath(String urlPath);
如果没有为MultiActionController指定任何MethodNameResolver,那么InternalPathMethodNameResolver将作为默认的MethodNameResolver,以进行Web请求与具体处理方法间的映射解析。
InternalPathMethodNameResolver将提取URL最后一个/之后的部分并去除扩展名,作为要返回的方法名称,比如:
我们还可以通过prefix和suffix来为上面截取得到的methodName加上前后缀,作为最终会去查询得到的方法名:
<bean id="internalPathMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver">
<property name="prefix" value="dhy_"/>
<property name="suffix" value="_xpy"/>
</bean>
如果加上了前后缀限制,那么上面的请求URL最终映射得到的方法名如下:
InternalPathMethodNameResolver的源码实现也非常简单:
public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolver {
private String prefix = "";
private String suffix = "";
private final Map<String, String> methodNameCache = new ConcurrentHashMap<String, String>(16);
//....省略get和set方法
//根据URL路径去获取到对应的handlerName
@Override
protected String getHandlerMethodNameForUrlPath(String urlPath) {
//首选查询缓存
String methodName = this.methodNameCache.get(urlPath);
if (methodName == null) {
//提取URL最后一个/之后的部分并去除扩展名,作为要返回的方法名称
methodName = extractHandlerMethodNameFromUrlPath(urlPath);
//加上前后缀
methodName = postProcessHandlerMethodName(methodName);
this.methodNameCache.put(urlPath, methodName);
}
//返回
return methodName;
}
protected String extractHandlerMethodNameFromUrlPath(String uri) {
return WebUtils.extractFilenameFromUrlPath(uri);
}
protected String postProcessHandlerMethodName(String methodName) {
return getPrefix() + methodName + getSuffix();
}
}
PropertiesMethodNameResolver与InternalPathMethodNameResolver唯一的相同点在于,他们都是基于请求的URL进行映射(因为都继承了AbstractUrlMethodNameResolver)。
但它比InternalPathMethodNameResolver灵活,如果HandlerMapping与MethodNameResolver都是处理映射这一点来看,InternalPathMethodNameResolver相当于BeanNameURLHandlerMapping,而PropertiesMethodNameResolver相当于SimpleUrlHandlerMapping.
PropertiesMethodNameResolver可以指定完全匹配的映射关系,或者使用ANT形式的路径匹配模式所表达的映射关系。
例如:
<bean id="propertiesMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
<property name="mappings">
<value>
/list.do=list
/update=update
/list*=list
</value>
</property>
</bean>
PropertiesMethodNameResolver的源码也非常简单:
public class PropertiesMethodNameResolver extends AbstractUrlMethodNameResolver
implements InitializingBean {
private Properties mappings;
private PathMatcher pathMatcher = new AntPathMatcher();
...
@Override
protected String getHandlerMethodNameForUrlPath(String urlPath) {
//先进行精确匹配
String methodName = this.mappings.getProperty(urlPath);
if (methodName != null) {
return methodName;
}
//如果精确匹配没找到,再进行模糊匹配
Enumeration propNames = this.mappings.propertyNames();
while (propNames.hasMoreElements()) {
String registeredPath = (String) propNames.nextElement();
//这里按顺序挨个匹配,当遇到第一个匹配成功,则直接返回
//因此不会进行最佳匹配
if (this.pathMatcher.match(registeredPath, urlPath)) {
return (String) this.mappings.get(registeredPath);
}
}
return null;
}
}
ParameterMethodNameResolver允许我们根据请求中的某个参数的值作为映射的方法名,也允许我们使用请求中的一组参数来处理映射的方法名称.
(1) 根据请求中的某个参数的值作为映射后的方法名。在Web请求提交之后,我们可以附带一个参数,专门指定由MultiActionController中的那个方法来处理当前请求
ParameterMethodNameResolver默认检测的参数名称为action,我们也可以通过setParamName方法来修改默认的参数名称。
<bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="paramName" value="methodName"/>
</bean>
那么,HTTP GET形式发送的URL看起来就类似于: http://host:port//dhy/xpy?methodName=list.
(2) 根据请求中的一组参数作为映射后的方法名,在某个页面中存在多种行为选择的时候,可以让每一种行为对应一个参数。
我们通过methodParamNames属性为ParameterMethodNameResolver指定一组要检测的参数名。
ParameterMethodNameResolver将以指定的一组参数名作为基准,对Web请求中的参数进行检测。
如果发现存在其中某个参数,则将当前Web请求映射到与参数相同名称的处理方法。
我们还可以通过指定defaultMethodName,来提供一个兜底方法,该方法会在请求无法找到合适处理方法的时候,作为默认方法进行处理
<bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="paramName" value="methodName"/>
<property name="methodParamNames" value="list,update,delete"/>
<property name="defaultMethodName" value="list"/>
</bean>
如果像上面一样,同时指定paramName和methodParamNames,那么谁的优先级高呢?
最简单的策略是楼一眼ParameterMethodNameResolver的源码:
public class ParameterMethodNameResolver implements MethodNameResolver {
/**
默认的映射请求参数名
*/
public static final String DEFAULT_PARAM_NAME = "action";
private String paramName = DEFAULT_PARAM_NAME;
private String[] methodParamNames;
//逻辑映射--下面源码中会体现出它的作用
private Properties logicalMappings;
private String defaultMethodName;
...
public String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException {
String methodName = null;
//首先处理methodParamNames
if (this.methodParamNames != null) {
for (String candidate : this.methodParamNames) {
//按顺序挨个匹配,如果有一个匹配上了,直接返回
if (WebUtils.hasSubmitParameter(request, candidate)) {
//这里是直接将methodParamNames数组中当前元素作为方法名
methodName = candidate;
if (logger.isDebugEnabled()) {
logger.debug("Determined handler method '" + methodName +
"' based on existence of explicit request parameter of same name");
}
break;
}
}
}
//如果methodParamNames没有匹配上,那么再对paramName进行处理
if (methodName == null && this.paramName != null) {
//这里是取出请求参数中的值作为方法名
methodName = request.getParameter(this.paramName);
if (methodName != null) {
if (logger.isDebugEnabled()) {
logger.debug("Determined handler method '" + methodName +
"' based on value of request parameter '" + this.paramName + "'");
}
}
}
//如果上面经过上面处理后,匹配上了,那么下面会把当前methodName作为一个逻辑值,去logicalMappings找到其真正的值
//前提是设置了logicalMappings ---相当于又提供了一层映射,感觉没啥用
if (methodName != null && this.logicalMappings != null) {
// Resolve logical name into real method name, if appropriate.
String originalName = methodName;
//第二个参数是如果找不到,默认值是啥
methodName = this.logicalMappings.getProperty(methodName, methodName);
if (logger.isDebugEnabled()) {
logger.debug("Resolved method name '" + originalName + "' to handler method '" + methodName + "'");
}
}
//防止上面得到的methodName 是空串
if (methodName != null && !StringUtils.hasText(methodName)) {
if (logger.isDebugEnabled()) {
logger.debug("Method name '" + methodName + "' is empty: treating it as no method name found");
}
methodName = null;
}
//如果一番折腾后,没得到,那么使用默认的defaultMethodName
if (methodName == null) {
if (this.defaultMethodName != null) {
// No specific method resolved: use default method.
methodName = this.defaultMethodName;
if (logger.isDebugEnabled()) {
logger.debug("Falling back to default handler method '" + this.defaultMethodName + "'");
}
}
else {
// If resolution failed completely, throw an exception.
throw new NoSuchRequestHandlingMethodException(request);
}
}
return methodName;
}
}
这里我们实现一个Stu的CRUD:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<br>
<h1>大忽悠首页</h1></br></br></br></br>
<a href="/stu">点击跳转到学生信息管理界面</a>
</body>
</html>
为MultiActionController提供一个委派对象的好处在于,委派对象不需要继承任何父类或者接口。因此这里采用委派对象。
并且使用ParamterMethodNameResolver对请求方法进行映射,具体来说,就是为methodParamNames指定insert,update,delete作为要映射的参数,同时以list作为默认的方法:
<bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="methodParamNames" value="insert,update,delete"/>
<property name="defaultMethodName" value="list"/>
</bean>
这样的话,我们的委派对象就需要实现list,create,update,delete对应名称的方法:
@Data
public class StuController{
private StuService stuService;
private String listViewName;
private String updateViewName;
private String insertViewName;
private String deleteViewName;
public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
return new ModelAndView(listViewName,"stuInfoList",stuService.list());
}
public ModelAndView insert(HttpServletRequest request, HttpServletResponse response){
return new ModelAndView(insertViewName);
}
public ModelAndView update(HttpServletRequest request, HttpServletResponse response){
stuService.update(Integer.valueOf(request.getParameter("sno")),request.getParameter("sname"));
return new ModelAndView(updateViewName);
}
public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, Stu stu){
stuService.delete(stu.getNum());
return new ModelAndView(deleteViewName);
}
}
<?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="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
<property name="methodParamNames" value="insert,update,delete"/>
<property name="defaultMethodName" value="list"/>
</bean>
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/**/stu">multiActionController</prop>
</props>
</property>
</bean>
<bean id="multiActionController" class="org.springframework.web.servlet.mvc.multiaction.MultiActionController">
<property name="delegate" ref="stuController"/>
<property name="methodNameResolver" ref="parameterMethodNameResolver"/>
</bean>
<bean id="stuController" class="com.example.controller.StuController">
<property name="stuService" ref="stuService"/>
<property name="listViewName" value="list"/>
<property name="updateViewName" value="update"/>
<property name="insertViewName" value="insert"/>
<property name="deleteViewName" value="delete"/>
</bean>
</beans>
通过multiActionController的delegate属性,我们指定了它要使用的委派对象,multiActionController内部将使用反射机制调用相应MethodNameResolver所返回的处理方法。
指定的逻辑视图名可以添加相应的前缀(prefix),比如: redirect:viewName,将以重定向的形式跳转到相应的视图,有关视图的转发和重定向将在后面ViewResolver和View部分进行介绍。
注意,需要将web.xml中DispathcerServlet的拦截路径修改为/
<servlet>
<servlet-name>controller</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>controller</servlet-name>
<!-- 拦截除*.jsp以外的所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
//无参构造,那么代理对象就是自身,即我们采用的方法是继承MultiActionController类
public MultiActionController() {
this.delegate = this;
//将当前类包括其父类内部所有public方法进行注册
registerHandlerMethods(this.delegate);
}
//有参构造传入代理对象
public MultiActionController(Object delegate) {
setDelegate(delegate);
}
//配置文件中指定属性注入
public final void setDelegate(Object delegate) {
Assert.notNull(delegate, "Delegate must not be null");
this.delegate = delegate;
//注册代理对象内部的方法
registerHandlerMethods(this.delegate);
// There must be SOME handler methods.
if (this.handlerMethodMap.isEmpty()) {
throw new IllegalStateException("No handler methods in class [" + this.delegate.getClass() + "]");
}
}
private void registerHandlerMethods(Object delegate) {
//清空
this.handlerMethodMap.clear();
this.lastModifiedMethodMap.clear();
this.exceptionHandlerMap.clear();
// Look at all methods in the subclass, trying to find
// methods that are validators according to our criteria
//先获取代理类及其父类中所有的public包括protected方法
Method[] methods = delegate.getClass().getMethods();
for (Method method : methods) {
// 判断该方法是否是用于异常兜底处理方法
if (isExceptionHandlerMethod(method)) {
//如果是的话,就进行注册
registerExceptionHandlerMethod(method);
}
//否则判断是否是普通处理方法
else if (isHandlerMethod(method)) {
//是的话就注册
registerHandlerMethod(method);
registerLastModifiedMethodIfExists(delegate, method);
}
}
}
上面提到了异常兜底处理方法,下面简单介绍一下使用
//方法名任意,最后一个参数表示当前方法用来处理哪种类型的异常
public ModelAndView anyMeaningfulName
(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);
public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws IllegalArgumentException {
if(true){
throw new IllegalArgumentException("测试兜底异常捕获");
}
return new ModelAndView(listViewName,"stuInfoList",stuService.list());
}
public ModelAndView catchIllegalArgumentException(HttpServletRequest request, HttpServletResponse response,IllegalArgumentException exception){
return new ModelAndView("error","ex",exception.getMessage());
}
private boolean isExceptionHandlerMethod(Method method) {
//首先需要满足普通handlerMethod的格式
return (isHandlerMethod(method) &&
//方法参数正好等于三个
method.getParameterTypes().length == 3 &&
//最后一个参数必须继承至Throwable
Throwable.class.isAssignableFrom(method.getParameterTypes()[2]));
}
private void registerExceptionHandlerMethod(Method method) {
//加入exceptionHandlerMap,key是异常类型,val是对应异常兜底方法
this.exceptionHandlerMap.put(method.getParameterTypes()[2], method);
if (logger.isDebugEnabled()) {
logger.debug("Found exception handler method [" + method + "]");
}
}
private boolean isHandlerMethod(Method method) {
//首先判断返回结果是否是下面规定的四种之一
Class returnType = method.getReturnType();
if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) ||
void.class.equals(returnType)) {
//再对参数值进行判断
Class[] parameterTypes = method.getParameterTypes();
//必须大于等于2,并且前面两个参数类型固定
return (parameterTypes.length >= 2 &&
HttpServletRequest.class.equals(parameterTypes[0]) &&
HttpServletResponse.class.equals(parameterTypes[1]) &&
//下面两个条件不能同时满足,否则说明是继承至Controller的方法
!("handleRequest".equals(method.getName()) && parameterTypes.length == 2));
}
return false;
}
private void registerHandlerMethod(Method method) {
if (logger.isDebugEnabled()) {
logger.debug("Found action method [" + method + "]");
}
//加入handlerMethodMap集合,key是方法名,val是对应的方法
this.handlerMethodMap.put(method.getName(), method);
}
因为继承了AbstractController,因此需要重写handleRequestInternal方法
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
try {
//方法解析器解析当前请求得到对应的方法名
String methodName = this.methodNameResolver.getHandlerMethodName(request);
//去调用代理对象的methodName
return invokeNamedMethod(methodName, request, response);
}
catch (NoSuchRequestHandlingMethodException ex) {
return handleNoSuchRequestHandlingMethod(ex, request, response);
}
}
protected final ModelAndView invokeNamedMethod(
String methodName, HttpServletRequest request, HttpServletResponse response) throws Exception {
//根据methodName从handlerMethodMap中得到对应的方法
Method method = this.handlerMethodMap.get(methodName);
//找不到,抛出异常
if (method == null) {
throw new NoSuchRequestHandlingMethodException(methodName, getClass());
}
//参数解析
try {
//首先获取到当前方法的参数列表
Class[] paramTypes = method.getParameterTypes();
//params参数数组大小为4,说明参数个数最多只有四个
List<Object> params = new ArrayList<Object>(4);
// 前面两个参数的位置和类型固定死了
params.add(request);
params.add(response);
//如果参数个数大于等于3个,并且第三个参数为HttpSession,那么设置进去
if (paramTypes.length >= 3 && paramTypes[2].equals(HttpSession.class)) {
HttpSession session = request.getSession(false);
if (session == null) {
throw new HttpSessionRequiredException(
"Pre-existing session required for handler method '" + methodName + "'");
}
params.add(session);
}
// 如果参数类型大于等于3个,并且最后一个参数不是HttpSession类型,这里可能是三个或者四个参数,
//如果是四个参数,那么第三个参数必须是HttpSession类型
if (paramTypes.length >= 3 &&
!paramTypes[paramTypes.length - 1].equals(HttpSession.class)) {
//实例化该对象,这里要求我们给出的自定义对象,必须要有无参构造才行
//因为这里是用无参构造实例化的对象
Object command = newCommandObject(paramTypes[paramTypes.length - 1]);
//加入参数集合
params.add(command);
//进行参数绑定
bind(request, command);
}
//调用代理对象的目标方法
Object returnValue = method.invoke(this.delegate, params.toArray(new Object[params.size()]));
//将返回值通通进行包装
return massageReturnValueIfNecessary(returnValue);
}
//如果出现异常,那么需要对异常进行处理
catch (InvocationTargetException ex) {
// The handler method threw an exception.
return handleException(request, response, ex.getTargetException());
}
catch (Exception ex) {
// The binding process threw an exception.
return handleException(request, response, ex);
}
}
protected void bind(HttpServletRequest request, Object command) throws Exception {
logger.debug("Binding request parameters onto MultiActionController command");
//创建请求数据绑定器
ServletRequestDataBinder binder = createBinder(request, command);
//进行数据绑定
binder.bind(request);
//validators进行校验
if (this.validators != null) {
for (Validator validator : this.validators) {
if (validator.supports(command.getClass())) {
ValidationUtils.invokeValidator(validator, command, binder.getBindingResult());
}
}
}
//对数据校验过程发生的异常进行处理
binder.closeNoCatch();
}
数据绑定过程简单理解就是按照请求参数名和对象属性名进行匹配,如果匹配成功,就设置进去。
validators校验这里暂时不展开,后面会讲。
private ModelAndView massageReturnValueIfNecessary(Object returnValue) {
if (returnValue instanceof ModelAndView) {
return (ModelAndView) returnValue;
}
else if (returnValue instanceof Map) {
return new ModelAndView().addAllObjects((Map) returnValue);
}
else if (returnValue instanceof String) {
return new ModelAndView((String) returnValue);
}
else {
// Either returned null or was 'void' return.
// We'll assume that the handle method already wrote the response.
return null;
}
}
private ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Throwable ex)
throws Exception {
//根据异常类型,去exceptionHandlerMap中取出对应处理该异常的方法
Method handler = getExceptionHandler(ex);
//如果不为空
if (handler != null) {
if (logger.isDebugEnabled()) {
logger.debug("Invoking exception handler [" + handler + "] for exception: " + ex);
}
try {
//则调用对应异常兜底方法,然后处理其返回值
Object returnValue = handler.invoke(this.delegate, request, response, ex);
return massageReturnValueIfNecessary(returnValue);
}
catch (InvocationTargetException ex2) {
logger.error("Original exception overridden by exception handling failure", ex);
ReflectionUtils.rethrowException(ex2.getTargetException());
}
catch (Exception ex2) {
logger.error("Failed to invoke exception handler method", ex2);
}
}
else {
//如果代理类目标方法执行过程抛出了异常,但是我们没有指定相关异常兜底方法,则抛出该异常
// If we get here, there was no custom handler or we couldn't invoke it.
ReflectionUtils.rethrowException(ex);
}
throw new IllegalStateException("Should never get here");
}
getExceptionHandler: 根据异常类型去exceptionHandlerMap获取对应的兜底方法
protected Method getExceptionHandler(Throwable exception) {
Class exceptionClass = exception.getClass();
if (logger.isDebugEnabled()) {
logger.debug("Trying to find handler for exception class [" + exceptionClass.getName() + "]");
}
Method handler = this.exceptionHandlerMap.get(exceptionClass);
//如果当前异常类型没匹配上,那么就调用抛出异常的父类型不断去匹配
while (handler == null && !exceptionClass.equals(Throwable.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Trying to find handler for exception superclass [" + exceptionClass.getName() + "]");
}
exceptionClass = exceptionClass.getSuperclass();
handler = this.exceptionHandlerMap.get(exceptionClass);
}
return handler;
}
本节主要对HandlerMapping和Controller相关类进行了简单介绍,下节,我们将继续探寻controller的更多细节实现,和整个DispathcerServlet执行流程