- 忙碌的协调人HandlerMapping
- 可用的HandlerMapping
- BeanNameUrlHandlerMapping
- SimpleUrlHandlerMapping
- HandlerMapping的执行序列(chain of handlermapping)
- 亲密伙伴Controller
- AbstractController
- MultiActionController
- MethodNameResolver
- InternalPathMethodNameResolver
- PropertiesMethodNameResolver
- ParameterMethodNameResolver
- MultiActionController使用演示
- MultiActionController源码剖析
- 小结
忙碌的协调人HandlerMapping
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;
}
- getHandler方法为什么返回HandlerExecutionChain
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关联的一堆拦截器
- 上面给出的一堆ATTRIBUTE,被各个HandlerMapping子实现类所支持,对应的子实现类,会取出自身支持的属性,然后根据结果做相关事情(这个下面会讲)
可用的HandlerMapping
Spring MVC默认提供了多个HandlerMapping的实现:
我们重点关注下面几个实现类:
- BeanNameUrlHandlerMapping: 用url最后一个资源名作为beanName去匹配对应的handler,例如: http://host:port/hhh.do会去当前容器内寻找名为/hhh.do的Controller定义。
- SimpleUrlHandlerMapping: 该实现类进一步接触了请求URL与Handler的beanName之间的耦合,并且支持更灵活的映射表达方式
- ControllerClassNameHandlerMapping: 我们将在后面章节再进行介绍。
- DefaultAnnotationHandlerMapping: Spring 2.5之后的Spring MVC引入了基于注解的配置方式。
BeanNameUrlHandlerMapping
BeanNameUrlHandlerMapping类的定义如下:
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
//当前handler可以映射到哪些URLS上去
@Override
protected String[] determineUrlsForHandler(String beanName) {
List 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的具体映射规则。
- 即用以"/“开头的beanName或者以”/"开头的别名,作为当前handler映射到的URLS数组
所以,如果要使用BeanNameUrlHandlerMapping,只需要在对应[servletName]-servlet.xml配置文件中,注入该bean即可: (当然,不注入的话,也会默认使用BeanNameUrlHandlerMapping)
SimpleUrlHandlerMapping
使用BeanNameUrlHandlerMapping进行Web请求到具体Handler的映射管理,需要我们保证视图模板中的请求路径,必须与容器中对应的Handler的beanName一致。
BeanNameUrlHandlerMapping的固定映射模式,并没有对映射做个过程做过多的关注,而是简单的直接匹配。
SimpleUrlHandlerMapping比BeanNameUrlHandlerMapping做的工作多一些,它可以使视图一方和handler一方自由活动,最后由SimpleUrlHandlerMapping进行统筹。
- SimpleUrlHandlerMapping基本使用如下:
helloController
这里所有请求后面要加.do是因为在web.xml中配置的DispatcherServlet拦截的路径是 * .do
现在控制器Controller可以起任何名字,视图中的链接也可以独立提供,只需要通过SimpleUrlHandlerMapping指定一下二者的对应关系即可。
SimpleUrlHandlerMapping 的源码也非常的简单:
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
//存放controller控制器和其对应映射到的请求路径
private final Map urlMap = new LinkedHashMap<>();
//下面两个方法对应xml中往urlMap中注入属性的两种不同方式
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
public void setUrlMap(Map urlMap) {
this.urlMap.putAll(urlMap);
}
public Map getUrlMap() {
return this.urlMap;
}
= //该方法会在父类中被调用,因为父类实现了ApplicationContextAware接口
//初始化时,会去调用setApplicationContext方法,其中会调用 initApplicationContext方法
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
//注册好handler的相关映射
registerHandlers(this.urlMap);
}
protected void registerHandlers(Map 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);
});
}
}
}
- 读取xml配置文件,初始化SimpleUrlHandlerMapping 时,会将我们在配置文件中设置好的请求映射放入urlMap集合中去
- 然后再初始化initApplicationContext时,又通过registerHandler,将相关映射加入父类的handlerMap映射集合中去
- 因为,每次请求来时,是去父类的handlerMap映射集合查找
SimpleUrlHandlerMapping还可以使用类似于ANT路径形式的模式匹配,这样我们就可以通过各种表达式,将一组或多组拥有某种相似特征的Web处理请求映射给相应的Handler处理。
helloController
当然,其实使用BeanNameUrlMapping也可以使用ANT路径匹配,只不过这里要使用ANT路径的地方,是对应控制器的beanName
为什么,可以玩呢?
- 其实,这和他们的父类AbstractUrlHandlerMapping中查找某个请求映射到的handler的lookupHandler方法有很大关系
- lookupHandler是查找请求映射handler的通用父类逻辑
@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 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 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 uriTemplateVariables = new LinkedHashMap<>();
for (String matchingPattern : matchingPatterns) {
if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
Map vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
Map 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;
}
先精确查找,再模糊匹配,如果存在多个匹配结果,那么精确优先。
HandlerMapping的执行序列(chain of handlermapping)
在基于Spirng MVC的Web应用程序中,我们可以为DispathcerServlet提供多个HandlerMapping,DispatcherServlet在选用HandlerMapping的过程中,会先对HandlerMapping集合按照优先级进行排序,然后按照依次询问每个HandlerMapping。
initHandlerMappings方法中会对HandlerMappings集合进行排序,并且该方法是在onRefresh方法中被调用的
如果某一个HandlerMapping能够返回可用的Handler,则DispathcerServlet使用当前Handler进行Web请求的处理,而不再询问其他HandlerMapping。
- 下面给出的是DispathcerServlet中获取某个请求对应handler的方法
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接口所规定的语义,数值越小,优先级越大,具体配置实例如下:
亲密伙伴Controller
Controller是Spring MVC框架支持的用于处理具体Web请求的Handler类型之一。
要实现一个具体的Controller,我们可以直接实现Controller接口:
@FunctionalInterface
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
- handleRequest方法解释: 处理请求并返回一个ModelAndView,DispathcerServlet会通过ModelAndView来进行渲染工作,如果返回null值,表示Controller内部自己完成了渲染,不需要DispathcerServlet来进行渲染
直接实现Controller接口当然没有问题,但是这需要我们去处理很多的通用细节问题,例如: 请求参数的抽取,请求编码的设定,国际化信息处理,Session数据的管理等。
而实际上,这些关注点是所有Controller都需要的,我们就应该想办法将这些通用的逻辑进行复用,这就是Spring MVC提供了一套Controller实现体系的原因。它帮助我们更好地处理了Web请求过程中的某些通用关注点。
Controller的继承体系如下:
- 自由挥洒派的Controller: 对于该派而言,其提供的就是接近原生Servlet API服务,我们从HttpServletRequest中获取请求参数,然后验证,调用业务层逻辑,最终返回一个ModelAndView。甚至,可以直接通过HttpServletResponse输出视图。虽然很自由,但是需要处理的细节也很多,因此大家可以根据使用场景,自行判断是否需要。
Spring 4开始,对原有的Controller架构进行了大调整,大家上面看到的这些类基本都被移除了,Spring 4开始的Controller架构如下,我们讲3,再看4
- 规范操作派的Controller: 以BASECommandController为首的规范操作派,对Web处理过程中某些通用逻辑进行了进一步的规范化封装处理,规范化的方面主要包括:
- 自动抽取请求参数并绑定到指定的Command对象
- 提供了统一的数据验证方式,BaseCommandController及其子类可以接收一组Validator以进行数据验证
- 规范化了表单的请求处理流程,并对简单的多页面表单请求处理提供支持
AbstractController
AbstractControlle是整个Controller继承体系的起源,该类通过模板方法模式帮助我们解决了如下几个通用关注点:
- 管理当前Controller所支持的请求方法类型(GET/POST)
- 是否需要session,如果需要尝试获取,但是不会选择创建,如果不存在,则抛出异常
- 管理页面的缓存设置,即是否允许浏览器缓存当前页面
- 管理执行流程在会话上的同步,session mutex
我们要做的,就是在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);
}
- WebContentGenerator父类实现的通用关注点分离方法:
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来完成业务逻辑编写,会发现还是需要关注参数抽取,数据验证等细节。
MultiActionController
该类在Spring 4中已经被移除了
MultiActionController用于对一组逻辑上相近的Web请求进行封装,例如针对同一个对象的CRUD进行封装。
而不需要像AbstaractController那样为每一个请求单独实现一个继承AbstaractController的处理类。
MultiActionController提供了以下功能:
- 请求参数到Command对象的绑定。
- 通过Validator的数据验证。
- 细化的异常处理方法。
为了在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便会在参数绑定时,去请求参数中尝试将同名属性绑定到对应的对象属性上面去
方法的返回值有三种类型,分别对应如下语义:
- 返回ModelAndView表示正常的Web处理方法,后继的ViewResolver和View处理流程,依照之前的DispathcerServlet的流程进行。
- 返回Map表明只返回了模型数据,而没有返回逻辑视图名。这时,将寻求默认的视图名。这个工作由org.springframework.web.servlet.RequestToViewNameTranslator负责,该类能按照某种规则提供一个默认的逻辑视图名。
- 返回void,则表明既没有返回模型数据,也没有返回逻辑视图名。这时,我们认为,当前Web请求处理方法自行处理掉了视图的渲染和输出。
- 另外,Spring 2.5中可以返回String ,代表逻辑视图名,没有相关的模型数据。
现在还有一个问题: 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
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);
InternalPathMethodNameResolver
如果没有为MultiActionController指定任何MethodNameResolver,那么InternalPathMethodNameResolver将作为默认的MethodNameResolver,以进行Web请求与具体处理方法间的映射解析。
InternalPathMethodNameResolver将提取URL最后一个/之后的部分并去除扩展名,作为要返回的方法名称,比如:
- /dhy/xpy/list.do ----> list
- /dhy/xpy/update.do ----> update
- /dhy/xpy/delete.do ----> delete
- /dhy/xpy/create.do ----> create
我们还可以通过prefix和suffix来为上面截取得到的methodName加上前后缀,作为最终会去查询得到的方法名:
如果加上了前后缀限制,那么上面的请求URL最终映射得到的方法名如下:
- /dhy/xpy/list.do ----> dhy_list_xpy
- /dhy/xpy/update.do ----> dhy_update_xpy
- /dhy/xpy/delete.do ----> dhy_delete_xpy
- /dhy/xpy/create.do ----> dhy_create_xpy
InternalPathMethodNameResolver的源码实现也非常简单:
public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolver {
private String prefix = "";
private String suffix = "";
private final Map methodNameCache = new ConcurrentHashMap(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
PropertiesMethodNameResolver与InternalPathMethodNameResolver唯一的相同点在于,他们都是基于请求的URL进行映射(因为都继承了AbstractUrlMethodNameResolver)。
但它比InternalPathMethodNameResolver灵活,如果HandlerMapping与MethodNameResolver都是处理映射这一点来看,InternalPathMethodNameResolver相当于BeanNameURLHandlerMapping,而PropertiesMethodNameResolver相当于SimpleUrlHandlerMapping.
PropertiesMethodNameResolver可以指定完全匹配的映射关系,或者使用ANT形式的路径匹配模式所表达的映射关系。
例如:
/list.do=list /update=update /list*=list
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
ParameterMethodNameResolver允许我们根据请求中的某个参数的值作为映射的方法名,也允许我们使用请求中的一组参数来处理映射的方法名称.
- 下面是两种策略的详细情况
(1) 根据请求中的某个参数的值作为映射后的方法名。在Web请求提交之后,我们可以附带一个参数,专门指定由MultiActionController中的那个方法来处理当前请求
ParameterMethodNameResolver默认检测的参数名称为action,我们也可以通过setParamName方法来修改默认的参数名称。
那么,HTTP GET形式发送的URL看起来就类似于: http://host:port//dhy/xpy?methodName=list.
(2) 根据请求中的一组参数作为映射后的方法名,在某个页面中存在多种行为选择的时候,可以让每一种行为对应一个参数。
我们通过methodParamNames属性为ParameterMethodNameResolver指定一组要检测的参数名。
ParameterMethodNameResolver将以指定的一组参数名作为基准,对Web请求中的参数进行检测。
如果发现存在其中某个参数,则将当前Web请求映射到与参数相同名称的处理方法。
我们还可以通过指定defaultMethodName,来提供一个兜底方法,该方法会在请求无法找到合适处理方法的时候,作为默认方法进行处理
如果像上面一样,同时指定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;
}
}
MultiActionController使用演示
这里我们实现一个Stu的CRUD:
- 先准备一个首页,当我们点击首页跳转按钮时,会跳转到学生管理界面,可以进行增删改查
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
大忽悠首页
点击跳转到学生信息管理界面


