阅读完需:约 59 分钟
之前已经大概介绍过web九大组件,本文将聚焦于Spring MVC中最重要的一个组件:HandlerMapping展开讨论
在最早之前也是学过HandlerMapping的,不过那时候是粗浅学习
可以在这里看看总览
依赖版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
HandlerMapping

HandlerMapping用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理呢?这就是HandlerMapping需要做的事。
HandlerMapping:负责映射用户的URL和对应的处理类Handler,HandlerMapping并没有规定这个URL与应用的处理类如何映射。所以在HandlerMapping接口中仅仅定义了根据一个URL必须返回一个由HandlerExecutionChain代表的处理链,我们可以在这个处理链中添加任意的HandlerAdapter实例来处理这个URL对应的请求(这样保证了最大的灵活性映射关系)。
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;
}
它有两大继承主线:MatchableHandlerMapping和AbstractHandlerMapping,其他的都是一些辅助接口。

AbstractHandlerMethodMapping 体系下的都是根据方法名进行匹配的,而 AbstractUrlHandlerMapping 体系下的都是根据 URL 路径进行匹配的,这两者有一个共同的父类 AbstractHandlerMapping,接下来我们就对这三个关键类进行详细分析。
AbstractHandlerMapping

AbstractHandlerMapping 实现了 HandlerMapping 接口,无论是通过 URL 进行匹配还是通过方法名进行匹配,都是通过继承 AbstractHandlerMapping 来实现的,所以 AbstractHandlerMapping 所做的事情其实就是一些公共的事情,将以一些需要具体处理的事情则交给子类去处理,这其实就是典型的模版方法模式。

有必要先对两个XXXSupport进行一个非常简单的说明~
WebApplicationObjectSupport和ApplicationObjectSupport
看他两的申明,他俩更像是ApplicationContextAware和ServletContextAware的适配器
public abstract class ApplicationObjectSupport implements ApplicationContextAware { ... }
public abstract class WebApplicationObjectSupport extends ApplicationObjectSupport implements ServletContextAware { ... }
所以我们如果我们在继承允许的情况下,只需要继承此类就能自动拥有上面两个接口的功能了。
@Service
public class HelloServiceImpl extends WebApplicationObjectSupport implements HelloService {
@Override
public Object hello() {
// 继承自ApplicationObjectSupport就可以很方便的获取到下面这两个值
System.out.println(super.getApplicationContext());
System.out.println(super.obtainApplicationContext()); //@since 5.0
// MessageSourceAccessor参考:MessageSourceAware 它是对MessageSource的一个包装 处理国际化
System.out.println(super.getMessageSourceAccessor());
// 这里需要继承和web相关的:WebApplicationObjectSupport
System.out.println(super.getWebApplicationContext());
System.out.println(super.getServletContext());
System.out.println(super.getTempDir()); //Tomcat9_demowar\work\Catalina\localhost\demo_war_war
return "service hello";
}
@Override
protected void initApplicationContext() throws BeansException {
// 这是父类提供给子类的(父类为空实现~),子类可以自行实现,实现子类的逻辑
// 比如子类AbstractDetectingUrlHandlerMapping就复写了此方法去detectHandlers();
super.initApplicationContext();
}
}
就这样可以通过继承的方式快速的实现获取上下文等,推荐使用~~~
WebApplicationObjectSupport用于提供上下文ApplicationContext和ServletContext的功能~
很显然如果你已经有继承了,那就没办法只能选择实现接口的方式了~
继续来看看AbstractHandlerMapping这个抽象实现给我们做了哪些事情~
// 它自己又额外实现了BeanNameAware和Ordered排序接口
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
//默认的Handler,这边使用的Obejct,子类实现的时候,使用HandlerMethod,HandlerExecutionChain等
// the default handler for this handler mapping
@Nullable
private Object defaultHandler;
// url路径计算的辅助类、工具类
private UrlPathHelper urlPathHelper = new UrlPathHelper();
// Ant风格的Path匹配模式~ 解决如/books/{id}场景
private PathMatcher pathMatcher = new AntPathMatcher();
// 保存着拦截器们~~~
private final List<Object> interceptors = new ArrayList<>();
// 从interceptors中解析得到,直接添加给全部handler
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
// 跨域相关的配置~
private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
// 最低的顺序(default: same as non-Ordered)
private int order = Ordered.LOWEST_PRECEDENCE;
@Nullable
private String beanName;
...
// 关于UrlPathHelper 的属性的一些设置~~~
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {...}
public void setUrlDecode(boolean urlDecode) { ... }
public void setRemoveSemicolonContent(boolean removeSemicolonContent) { ... }
public void setUrlPathHelper(UrlPathHelper urlPathHelper) { ... } //我们也是可议自己指定一个自己的UrlPathHelper 的
...
// PathMatcher我们也可以自己指定
public void setPathMatcher(PathMatcher pathMatcher) { ... }
// Set the interceptors to apply for all handlers mapped by this handler mapping
// 可变参数:可以一次性添加多个拦截器~~~~ 这里使用的Object
public void setInterceptors(Object... interceptors) {
this.interceptors.addAll(Arrays.asList(interceptors));
}
// 设值一个UrlBasedCorsConfigurationSource Map表示它的一些属性们~~~
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) { ... }
// 重载方法 @since 5.1 Spring5.1之后才有的方法
public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
this.corsConfigurationSource = corsConfigurationSource;
}
// Configure a custom {@link CorsProcessor} to use to apply the matched
// @since 4.2
public void setCorsProcessor(CorsProcessor corsProcessor) {
Assert.notNull(corsProcessor, "CorsProcessor must not be null");
this.corsProcessor = corsProcessor;
}
...
// 这步骤是最重要的。相当于父类setApplicationContext完成了之后,就会执行到这里~~~
// 这这步骤可议看出 这里主要处理的都是拦截器~~~相关的内容
@Override
protected void initApplicationContext() throws BeansException {
// 给子类扩展:增加拦截器,默认为空实现
extendInterceptors(this.interceptors);
// 找到所有MappedInterceptor类型的bean添加到adaptedInterceptors中
detectMappedInterceptors(this.adaptedInterceptors);
// 将interceptors中的拦截器取出放入adaptedInterceptors
// 如果是WebRequestInterceptor类型的拦截器 需要用WebRequestHandlerInterceptorAdapter进行包装适配
initInterceptors();
}
// extendInterceptors 是一个模版方法,可以在子类中实现,子类实现了该方法之后,可以对拦截器进行添加、删除或者修改,
// 不过在 SpringMVC 的具体实现中,其实这个方法并没有在子类中进行实现。
protected void extendInterceptors(List<Object> interceptors) {
}
// 去容器(含祖孙容器)内找到所有的MappedInterceptor类型的拦截器出来,添加进去 非单例的Bean也包含
// 备注MappedInterceptor为Spring MVC拦截器接口`HandlerInterceptor`的实现类 并且是个final类 Spring3.0后出来的。
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
obtainApplicationContext(), MappedInterceptor.class, true, false).values());
}
// 它就是把调用者放进来的interceptors们,适配成HandlerInterceptor然后统一放在`adaptedInterceptors`里面装着~~~
protected void initInterceptors() {
if (!this.interceptors.isEmpty()) {
for (int i = 0; i < this.interceptors.size(); i++) {
Object interceptor = this.interceptors.get(i);
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
// 适配其实也很简单~就是支持源生的HandlerInterceptor以及WebRequestInterceptor两种情况而已
protected HandlerInterceptor adaptInterceptor(Object interceptor) {
if (interceptor instanceof HandlerInterceptor) {
return (HandlerInterceptor) interceptor;
} else if (interceptor instanceof WebRequestInterceptor) {
// WebRequestHandlerInterceptorAdapter它就是个`HandlerInterceptor`,内部持有一个WebRequestInterceptor的引用而已
// 内部使用到了DispatcherServletWebRequest包request和response包装成`WebRequest`等等
return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
} else {
throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName());
}
}
protected final HandlerInterceptor[] getAdaptedInterceptors() { ... }
// 它只会返回MappedInterceptor这种类型的,上面是返回adaptedInterceptors所有
protected final MappedInterceptor[] getMappedInterceptors() { ... }
// 这个方法也是一个该抽象类提供的一个非常重要的模版方法:根据request获取到一个HandlerExecutionChain
// 也是抽象类实现接口HandlerMapping的方法~~~
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 根据request获取对应的handler 抽象方法,由具体的子类去实现~~~~
Object handler = getHandlerInternal(request);
// 若没有匹配上处理器,那就走默认的处理器~~~ 默认的处理器也是需要由子类给赋值 否则也会null的
if (handler == null) {
handler = getDefaultHandler();
}
// 若默认的处理器都木有 那就直接返回null啦~
if (handler == null) {
return null;
}
// 意思是如果是个String类型的名称,那就去容器内找这个Bean,当作一个Handler~
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// 确保 lookupPath 存在
if (!ServletRequestPathUtils.hasCachedPath(request)) {
initLookupPath(request);
}
// 关键步骤:根据handler和request构造一个请求处理链~~
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
// 4.2版本提供了对CORS跨域资源共享的支持 此处暂时略过~
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
if (getCorsConfigurationSource() != null) {
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
config = (globalConfig != null ? globalConfig.combine(config) : config);
}
if (config != null) {
config.validateAllowCredentials();
}
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
// 已经找到handler了,那就根据此构造一个请求链
// 这里主要是吧拦截器们给糅进来~ 构成对指定请求的一个拦截器链
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
// 小细节:因为handler本身也许就是个Chain,所以此处需要判断一下~
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
// 此处就用到了urlPathHelper来解析request
// 如我的请求地址为:`http://localhost:8080/demo_war_war/api/v1/hello` 那么lookupPath=/api/v1/hello
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
// 这里其实就能体现出MappedInterceptor的些许优势了:也就是它只有路径匹配上了才会拦截,没有匹配上的就不会拦截了,处理起来确实是更加的优雅些了~~~~
// 备注:MappedInterceptor可以设置includePatterns和excludePatterns等~~~~~
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
...
}
AbstractHandlerMapping主要实现了对方法getHandler()的模版实现,它主要是对HandlerInterceptor进行了一个通用处理,最终会把他们放进HandlerExecutionChain里面去
AbstractHandlerMapping 的初始化其实也就是拦截器的初始化过程。
为什么 AbstractHandlerMapping 中对拦截器如此重视呢?其实不是重视,AbstractUrlHandlerMapping 和 AbstractHandlerMethodMapping 最大的区别在于查找处理器的区别,一旦处理器找到了,再去找拦截器,但是拦截器都是统一的,并没有什么明显区别,所以拦截器就统一在 AbstractHandlerMapping 中进行处理,而不会去 AbstractUrlHandlerMapping 或者 AbstractHandlerMethodMapping 中处理。
来看一下getHandler方法的流程
- 首先调用 getHandlerInternal 方法去尝试获取处理器,getHandlerInternal 方法也是一个模版方法,该方法将在子类中实现。
- 如果没找到相应的处理器,则调用 getDefaultHandler 方法获取默认的处理器,我们在配置 HandlerMapping 的时候可以配置默认的处理器。
- 如果找到的处理器是一个字符串,则根据该字符串找去 SpringMVC 容器中找到对应的 Bean。
- 确保 lookupPath 存在,一会找对应的拦截器的时候会用到。
- 找到 handler 之后,接下来再调用 getHandlerExecutionChain 方法获取 HandlerExecutionChain 对象。
- 接下来 if 里边的是进行跨域处理的,获取到跨域的相关配置,然后进行验证&配置,检查是否允许跨域。
这里直接根据已有的 handler 创建一个新的 HandlerExecutionChain 对象,然后遍历 adaptedInterceptors 集合,该集合里存放的都是拦截器,如果拦截器的类型是 MappedInterceptor,则调用 matches 方法去匹配一下,看一下是否是拦截当前请求的拦截器,如果是,则调用 chain.addInterceptor 方法加入到 HandlerExecutionChain 对象中;如果就是一个普通拦截器,则直接加入到 HandlerExecutionChain 对象中。
AbstractUrlHandlerMapping

从命名中也能看出来,它和URL有关。它的大致思路为:将url对应的Handler保存在一个Map中,在getHandlerInternal方法中使用url从Map中获取Handler
从 getHandlerInternal 方法开始看起
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
Object handler;
if (usesPathPatterns()) {
RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
handler = lookupHandler(path, lookupPath, request);
}
else {
handler = lookupHandler(lookupPath, request);
}
if (handler == null) {
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if (StringUtils.matchesCharacter(lookupPath, '/')) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}
- 首先找到 lookupPath,就是请求的路径。
- 接下来就是调用
lookupHandler方法获取Handler对象,lookupHandler有一个重载方法,具体用哪个,主要看所使用的 URL 匹配模式,如果使用了最新的PathPattern(Spring5 之后的),则使用三个参数的lookupHandler;如果还是使用之前旧的AntPathMatcher,则这里使用两个参数的lookupHandler。 - 如果前面没有获取到 handler 实例,则接下来再做各种尝试,去分别查找
RootHandler、DefaultHandler等,如果找到的 Handler 是一个 String,则去 Spring 容器中查找该 String 对应的 Bean,再调用 validateHandler 方法来校验找到的 handler 和 request 是否匹配,不过这是一个空方法,子类也没有实现,所以可以忽略之。最后再通过 buildPathExposingHandler 方法给找到的 handler 添加一些参数。
这就是整个 getHandlerInternal 方法的逻辑,实际上并不难,里边主要涉及到 lookupHandler 和 buildPathExposingHandler 两个方法
lookupHandler
lookupHandler 有两个,我们分别来看。
@Nullable
protected Object lookupHandler(String lookupPath, HttpServletRequest request) throws Exception {
Object handler = getDirectMatch(lookupPath, request);
if (handler != null) {
return handler;
}
// Pattern match?
List<String> matchingPatterns = new ArrayList<>();
for (String registeredPattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(registeredPattern, lookupPath)) {
matchingPatterns.add(registeredPattern);
}
else if (useTrailingSlashMatch()) {
if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", lookupPath)) {
matchingPatterns.add(registeredPattern + "/");
}
}
}
String bestMatch = null;
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
if (!matchingPatterns.isEmpty()) {
matchingPatterns.sort(patternComparator);
bestMatch = matchingPatterns.get(0);
}
if (bestMatch != null) {
handler = this.handlerMap.get(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, lookupPath);
// 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, lookupPath);
Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
uriTemplateVariables.putAll(decodedVars);
}
}
return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
}
// No handler found...
return null;
}
@Nullable
private Object getDirectMatch(String urlPath, HttpServletRequest request) throws Exception {
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath, urlPath, null);
}
return null;
}
- 这里首先调用
getDirectMatch方法直接去handlerMap中找对应的处理器,handlerMap中就保存了请求 URL 和处理器的映射关系,具体的查找过程就是先去handlerMap中找,找到了,如果是 String,则去 Spring 容器中找对应的 Bean,然后调用validateHandler方法去验证(实际上没有验证,前面已经说了),最后调用buildPathExposingHandler方法添加拦截器。 - 如果
getDirectMatch方法返回值不为 null,则直接将查找到的 handler 返回,方法到此为止。那么什么情况下getDirectMatch方法的返回值不为 null 呢?简单来收就是没有使用通配符的情况下,请求地址中没有通配符,一个请求地址对应一个处理器,只有这种情况,getDirectMatch方法返回值才不为 null,因为handlerMap中保存的是代码的定义,比如我们定义代码的时候,某个处理器的访问路径可能带有通配符,但是当我们真正发起请求的时候,请求路径里是没有通配符的,这个时候再去 handlerMap 中就找不对对应的处理器了。如果用到了定义接口时用到了通配符,则需要在下面的代码中继续处理。 -
接下来处理通配符的情况。首先定义
matchingPatterns集合,将当前请求路径和 handlerMap 集合中保存的请求路径规则进行对比,凡是能匹配上的规则都直接存入matchingPatterns集合中。具体处理中,还有一个useTrailingSlashMatch的可能,有的小伙伴 SpringMVC 用的不熟练,看到这里可能就懵了,这里是这样的,SpringMVC 中,默认是可以匹配结尾/的,举个简单例子,如果你定义的接口是/user,那么请求路径可以是/user也可以/user/,这两种默认都是支持的,所以这里的useTrailingSlashMatch分支主要是处理后面这种情况,处理方式很简单,就在registeredPattern后面加上/然后继续和请求路径进行匹配。 -
由于一个请求 URL 可能会和定义的多个接口匹配上,所以
matchingPatterns变量是一个数组,接下来就要对matchingPatterns进行排序,排序完成后,选择排序后的第一项作为最佳选项赋值给bestMatch变量。默认的排序规则是AntPatternComparator,当然开发者也可以自定义。AntPatternComparator中定义的优先级如下:
| 路由配置 | 优先级 |
|---|---|
不含任何特殊符号的路径,如:配置路由/a/b/c
|
第一优先级 |
带有{}的路径,如:/a/{b}/c
|
第二优先级 |
带有正则的路径,如:/a/{regex:\d{3}}/c
|
第三优先级 |
带有*的路径,如:/a/b/*
|
第四优先级 |
带有**的路径,如:/a/b/**
|
第五优先级 |
最模糊的匹配:/**
|
最低优先级 |
- 找到
bestMatch之后,接下来再根据bestMatch去handlerMap中找到对应的处理器,直接找如果没找到,就去检查 bestMatch 是否以/结尾,如果是以/结尾,则去掉结尾的/再去handlerMap中查找,如果还没找到,那就该抛异常出来了。如果找到的 handler 是 String 类型的,则再去 Spring 容器中查找对应的 Bean,接下来再调用validateHandler方法进行验证。 - 接下来调用
extractPathWithinPattern方法提取出映射路径,例如定义的接口规则是myroot/*.html,请求路径是myroot/myfile.html,那么最终获取到的就是myfile.html。 -
接下来的 for 循环是为了处理存在多个最佳匹配规则的情况,在第四步中,我们对
matchingPatterns进行排序,排序完成后,选择第一项作为最佳选项赋值给bestMatch,但是最佳选项可能会有多个,这里就是处理最佳选项有多个的情况。 - 最后调用
buildPathExposingHandler方法注册两个内部拦截器。
lookupHandler 还有一个重载方法,不过只要大家把这个方法的执行流程搞清楚了,重载方法其实很好理解,重载方法用了 PathPattern 去匹配 URL 路径,而这个方法用了 AntPathMatcher 去匹配 URL 路径。
buildPathExposingHandler
protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern,
String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) {
HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping));
if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
}
return chain;
}
buildPathExposingHandler 方法向 HandlerExecutionChain 中添加了两个拦截器 PathExposingHandlerInterceptor 和 UriTemplateVariablesHandlerInterceptor,这两个拦截器在各自的 preHandle 中分别向 request 对象添加了一些属性
在前面的方法中,涉及到一个重要的变量 handlerMap,我们定义的接口和处理器之间的关系都保存在这个变量中,那么这个变量是怎么初始化的呢?这就涉及到 AbstractUrlHandlerMapping 中的另一个方法 registerHandler:
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Object resolvedHandler = handler;
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (getPatternParser() != null) {
this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
}
}
}
}
registerHandler(String[],String) 方法有两个参数,第一个就是定义的请求路径,第二个参数则是处理器 Bean 的名字,第一个参数是一个数组,那是因为同一个处理器可以对应多个不同的请求路径。
在重载方法 registerHandler(String,String) 里边,完成了 handlerMap 的初始化,具体流程如下:
- 如果没有设置
lazyInitHandlers,并且 handler 是 String 类型,那么就去 Spring 容器中找到对应的 Bean 赋值给resolvedHandler。 - 根据 urlPath 去 handlerMap 中查看是否已经有对应的处理器了,如果有的话,则抛出异常,一个 URL 地址只能对应一个处理器,这个很好理解。
- 接下来根据 URL 路径,将处理器进行配置,最终添加到 handlerMap 变量中。
这就是 AbstractUrlHandlerMapping 的主要工作,其中 registerHandler 将在它的子类中调用。
接下来我们来看 AbstractUrlHandlerMapping 的子类。
SimpleUrlHandlerMapping

它是AbstractUrlHandlerMapping的直接实现类,也是一个基于Map的简单实现。
为了方便处理,SimpleUrlHandlerMapping 中自己定义了一个 urlMap 变量,这样可以在注册之前做一些预处理,例如确保所有的 URL 都是以 / 开始。SimpleUrlHandlerMapping 在定义时重写了父类的 initApplicationContext 方法,并在该方法中调用了 registerHandlers,在 registerHandlers 中又调用了父类的 registerHandler 方法完成了 handlerMap 的初始化操作:
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.urlMap);
}
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.trace("No patterns in " + formatMappingName());
}
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();
}
registerHandler(url, handler);
});
}
}
这块代码很简单,实在没啥好说的,如果 URL 不是以 / 开头,则手动给它加上 / 即可。那 urlMap 的值从哪里来?当然是从我们的配置文件里边来呀,像下面这样:
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="urlMap">
<map>
<entry key="/aaa" value-ref="/hello"/>
</map>
</property>
</bean>
AbstractDetectingUrlHandlerMapping

AbstractDetectingUrlHandlerMapping 也是 AbstractUrlHandlerMapping 的子类,但是它和 SimpleUrlHandlerMapping 有一些不一样的地方。
AbstractDetectingUrlHandlerMapping 会自动查找到 SpringMVC 容器以及 Spring 容器中的所有 beanName,然后根据 beanName 解析出对应的 URL 地址,再将解析出的 url 地址和对应的 beanName 注册到父类的 handlerMap 变量中。换句话说,如果你用了 AbstractDetectingUrlHandlerMapping,就不用像 SimpleUrlHandlerMapping 那样去挨个配置 URL 地址和处理器的映射关系了。我们来看下
AbstractDetectingUrlHandlerMapping#initApplicationContext 方法:
@Override
public void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
detectHandlers();
}
protected void detectHandlers() throws BeansException {
ApplicationContext applicationContext = obtainApplicationContext();
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
applicationContext.getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
registerHandler(urls, beanName);
}
}
}
AbstractDetectingUrlHandlerMapping 重写了父类的 initApplicationContext 方法,并在该方法中调用了 detectHandlers 方法,在 detectHandlers 中,首先查找到所有的 beanName,然后调用 determineUrlsForHandler 方法分析出 beanName 对应的 URL,不过这里的 determineUrlsForHandler 方法是一个空方法,具体的实现在它的子类中,AbstractDetectingUrlHandlerMapping 只有一个子类。
BeanNameUrlHandlerMapping
BeanNameUrlHandlerMapping,我们一起来看下:
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
@Override
protected String[] determineUrlsForHandler(String beanName) {
List<String> urls = new ArrayList<>();
// 意思就是必须以/开头才行~~~~~~这算是一种约定吧~~~
// 这种方式和@WebServlet方式一毛一样~~~~~
if (beanName.startsWith("/")) {
urls.add(beanName);
}
// 当然别名也是可以的
String[] aliases = obtainApplicationContext().getAliases(beanName);
for (String alias : aliases) {
if (alias.startsWith("/")) {
urls.add(alias);
}
}
return StringUtils.toStringArray(urls);
}
}
这个类很简单,里边就一个 determineUrlsForHandler 方法,这个方法的执行逻辑也很简单,就判断 beanName 是不是以 / 开始,如果是,则将之作为 URL。
如果我们想要在项目中使用 BeanNameUrlHandlerMapping,配置方式如下:
<bean class="org.javaboy.init.HelloController" name="/hello"/>
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
</bean>
注意,Controller 的 name 必须是以 / 开始,否则该 bean 不会被自动作为处理器。
该实现就是根据bean的名称来匹配URL。方式同@WebServlet一模一样
至此,AbstractUrlHandlerMapping 体系下的东西就没了。
HandlerMapping,除了介绍它的抽象实现外。就是介绍了AbstractUrlHandlerMapping系列。
它主要实现是BeanNameUrlHandlerMapping和SimpleUrlHandlerMapping,属于Spring最早期的控制器实现。完全是基于类级别的:一个类就是一个Handler,模式和源生的Servlet没太大差异(还是和Servlet源生API有耦合的)。
显然这种模式在现在全注解时代已经完全过时了,开发、运行效率都太低下了。
从分析DispatcherServlet的时候发现,SpringMVC默认是给容器内注入了两个HandlerMapping组件的:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping由此可见Spring还是保持了充分的向下兼容的
AbstractHandlerMethodMapping

AbstractUrlHandlerMapping体系的实现原理分析:它是基于类级别的Handler实现,大体上和源生servlet如出一辙,也还没有脱离源生servlet的API。作为第一版的实现,便捷度自然存在一些欠缺,但大的框架还是非常稳的。
AbstractHandlerMethodMapping系列,基于方法级别的Handler实现。也是当下最为主流的实现方式,更是最为常用使用方式。
AbstractHandlerMethodMapping系列是将method作为handler来使用的,比如@RequestMapping所注释的方法就是这种handler(当然它并不强制你一定得使用@RequestMapping这样的注解)。
AbstractHandlerMethodMapping 体系下只有三个类,分别是 AbstractHandlerMethodMapping、RequestMappingInfoHandlerMapping 以及 RequestMappingHandlerMapping,如下图:

在前面的 AbstractUrlHandlerMapping 体系下,一个 Handler 一般就是一个类,但是在 AbstractHandlerMethodMapping 体系下,一个 Handler 就是一个 Mehtod,这也是我们目前使用 SpringMVC 时最常见的用法,即直接用 @RequestMapping 去标记一个方法,该方法就是一个 Handler。
// @since 3.1 Spring3.1之后才出现,这个时候注解驱动也出来了
// 实现了initializingBean接口,其实主要的注册操作则是通过afterPropertiesSet()接口方法来调用的
// 它是带有泛型T的。
// T:包含HandlerMethod与传入请求匹配所需条件的handlerMethod的映射~
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
// SCOPED_TARGET的BeanName的前缀
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
// 跨域相关
private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
static {
ALLOW_CORS_CONFIG.addAllowedOrigin("*");
ALLOW_CORS_CONFIG.addAllowedMethod("*");
ALLOW_CORS_CONFIG.addAllowedHeader("*");
ALLOW_CORS_CONFIG.setAllowCredentials(true);
}
// 默认不会去祖先容器里面找Handlers
private boolean detectHandlerMethodsInAncestorContexts = false;
// @since 4.1提供的新接口
// 为处HandlerMetho的映射分配名称的策略接口 只有一个方法getName()
// 唯一实现为:RequestMappingInfoHandlerMethodMappingNamingStrategy
// 策略为:@RequestMapping指定了name属性,那就以指定的为准 否则策略为:取出Controller所有的`大写字母` + # + method.getName()
// 如:AppoloController#match方法 最终的name为:AC#match
// 当然这个你也可以自己实现这个接口,然后set进来即可(只是一般没啥必要这么去干~~)
@Nullable
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
// 内部类:负责注册~
private final MappingRegistry mappingRegistry = new MappingRegistry();
// 此处细节:使用的是读写锁 比如此处使用的是读锁 获得所有的注册进去的Handler的Map
public Map<T, HandlerMethod> getHandlerMethods() {
this.mappingRegistry.acquireReadLock();
try {
return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
} finally {
this.mappingRegistry.releaseReadLock();
}
}
// 此处是根据mappingName来获取一个Handler 此处需要注意哦~~~
@Nullable
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
}
// 最终都是委托给mappingRegistry去做了注册的工作 此处日志级别为trace级别
public void registerMapping(T mapping, Object handler, Method method) {
if (logger.isTraceEnabled()) {
logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}
public void unregisterMapping(T mapping) {
if (logger.isTraceEnabled()) {
logger.trace("Unregister mapping \"" + mapping + "\"");
}
this.mappingRegistry.unregister(mapping);
}
// 这个很重要,是初始化HandlerMethods的入口~~~~~
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
// 看initHandlerMethods(),观察是如何实现加载HandlerMethod
protected void initHandlerMethods() {
// getCandidateBeanNames:Object.class相当于拿到当前容器(一般都是当前容器) 内所有的Bean定义信息
// 如果阁下容器隔离到到的话,这里一般只会拿到@Controller标注的web组件 以及其它相关web组件的 不会非常的多的~~~~
for (String beanName : getCandidateBeanNames()) {
// BeanName不是以这个打头得 这里才会process这个BeanName~~~~
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// 会在每个Bean里面找处理方法,HandlerMethod,然后注册进去
processCandidateBean(beanName);
}
}
// 略:它就是输出一句日志:debug日志或者trace日志 `7 mappings in 'requestMappingHandlerMapping'`
handlerMethodsInitialized(getHandlerMethods());
}
protected String[] getCandidateBeanNames() {
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
}
// 确定指定的候选bean的类型,如果标识为Handler类型,则调用DetectHandlerMethods
// isHandler(beanType):判断这个type是否为Handler类型 它是个抽象方法,由子类去决定到底啥才叫Handler~~~~
// `RequestMappingHandlerMapping`的判断依据为:该类上标注了@Controller注解或者@Controller注解 就算作是一个Handler
// 所以此处:@Controller起到了一个特殊的作用,不能等价于@Component的哟~~~~
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
} catch (Throwable ex) {
// 即使抛出异常 程序也不会终止~
}
if (beanType != null && isHandler(beanType)) {
// 这个和我们上篇博文讲述的类似,都属于detect探测系列~~~~
detectHandlerMethods(beanName);
}
}
// 在指定的Handler的bean中查找处理程序方法Methods 找打就注册进去:mappingRegistry
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 又是非常熟悉的方法:MethodIntrospector.selectMethods
// 它在我们招@EventListener、@Scheduled等注解方法时已经遇到过多次
// 此处特别之处在于:getMappingForMethod属于一个抽象方法,由子类去决定它的寻找规则~~~~ 什么才算作一个处理器方法
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
} catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex);
}
});
// 把找到的Method 一个个遍历,注册进去~~~~
methods.forEach((method, mapping) -> {
// 找到这个可调用的方法(AopUtils.selectInvocableMethod)
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
}
具体的初始化又是在 initHandlerMethods 方法中完成的,在该方法中,首先调用 getCandidateBeanNames 方法获取容器中所有的 beanName,然后调用 processCandidateBean 方法对这些候选的 beanName 进行处理,具体的处理思路就是根据 beanName 找到 beanType,然后调用 isHandler 方法判断该 beanType 是不是一个 Handler,isHandler 是一个空方法,在它的子类 RequestMappingHandlerMapping 中被实现了,该方法主要是检查该 beanType 上有没有 @Controller 或者 @RequestMapping 注解,如果有,说明这就是我们想要的 handler,接下来再调用 detectHandlerMethods 方法保存 URL 和 handler 的映射关系:
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
- 首先找到
handler的类型handlerType。 - 调用
ClassUtils.getUserClass方法检查是否是 cglib 代理的子对象类型,如果是,则返回父类型,否则将参数直接返回。 - 接下来调用
MethodIntrospector.selectMethods方法获取当前 bean 中所有符合要求的 method。 - 遍历 methods,调用
registerHandlerMethod方法完成注册。
上面这段代码里又涉及到两个方法:
getMappingForMethodregisterHandlerMethod
getMappingForMethod
getMappingForMethod 是一个模版方法,具体的实现也是在子类 RequestMappingHandlerMapping 里边:
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
首先根据 method 对象,调用 createRequestMappingInfo 方法获取一个 RequestMappingInfo,一个 RequestMappingInfo 包含了一个接口定义的详细信息,例如参数、header、produces、consumes、请求方法等等信息都在这里边。接下来再根据 handlerType 也获取一个 RequestMappingInfo,并调用 combine 方法将两个 RequestMappingInfo 进行合并。接下来调用 getPathPrefix 方法查看 handlerType 上有没有 URL 前缀,如果有,就添加到 info 里边去,最后将 info 返回。
这里要说一下 handlerType 里边的这个前缀是那里来的,我们可以在 Controller 上使用 @RequestMapping 注解,配置一个路径前缀,这样 Controller 中的所有方法都加上了该路径前缀,但是这种方式需要一个一个的配置,如果想一次性配置所有的 Controller 呢?我们可以使用 Spring5.1 中新引入的方法 addPathPrefix 来配置,如下:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setPatternParser(new PathPatternParser()).addPathPrefix("/enmalvi", HandlerTypePredicate.forAnnotation(RestController.class));
}
}
上面这个配置表示,所有的 @RestController 标记的类都自动加上 enmalvi 前缀。有了这个配置之后,上面的 getPathPrefix 方法获取到的就是 /enmalvi 了。
registerHandlerMethod
当找齐了 URL 和 handlerMethod 之后,接下来就是将这些信息保存下来,方式如下:
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
for (String path : directPaths) {
this.pathLookup.add(path, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
corsConfig.validateAllowCredentials();
this.corsLookup.put(handlerMethod, corsConfig);
}
this.registry.put(mapping,
new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
- 首先调用
createHandlerMethod方法创建HandlerMethod对象。 - 调用
validateMethodMapping方法对handlerMethod进行验证,主要是验证handlerMethod是否已经存在。 - 从 mappings 中提取出 directPaths,就是不包含通配符的请求路径,然后将请求路径和 mapping 的映射关系保存到 pathLookup 中。
-
找到所有 handler 的简称,调用 addMappingName 方法添加到 nameLookup 中。例如我们在 HelloController 中定义了一个名为 hello 的请求接口,那么这里拿到的就是
HC#hello,HC 是 HelloController 中的大写字母。 - 初始化跨域配置,并添加到 corsLookup 中。
- 将构建好的关系添加到 registry 中。
第四步这个东西有啥用呢?这个其实是 Spring4 中开始增加的功能,算是一个小彩蛋吧,虽然日常开发很少用
假如有一个接口:
@RestController
@RequestMapping("/javaboy")
public class HelloController {
@GetMapping("/aaa")
public String hello99() {
return "aaa";
}
}
当你请求该接口的时候,不想通过路径,想直接通过方法名,行不行呢?当然可以!
在 jsp 文件中,添加如下超链接:
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<a href="${s:mvcUrl('HC#hello99').build()}">Go!</a>
</body>
</html>
当这个 jsp 页面渲染完成后,href 属性就自动成了 hello99 方法的请求路径了。这个功能的实现,就依赖于前面第四步的内容。
该抽象类完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是决定哪些Bean是Handler类和哪些方法才是HandlerMathod,这些逻辑都是交给子类自己去实现,所以这层抽象可谓也是非常的灵活,并没有把Handler的实现方式定死,允许不同
这里面有个核心内容:那就是注册handlerMethod,是交给AbstractHandlerMethodMapping的一个内部类MappingRegistry去完成的,用来专门维持所有的映射关系,并提供方法去查找方法去提供当前url映射的方法。
class MappingRegistry {
// mapping对应的其MappingRegistration对象~~~
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
// 保存着mapping和HandlerMethod的对应关系~
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
// 保存着URL与匹配条件(mapping)的对应关系 当然这里的URL是pattern式的,可以使用通配符
// 这里的Map不是普通的Map,而是MultiValueMap,它是个多值Map。其实它的value是一个list类型的值
// 至于为何是多值?有这么一种情况 URL都是/api/v1/hello 但是有的是get post delete等方法 所以有可能是会匹配到多个MappingInfo的
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
// 这个Map是Spring MVC4.1新增的(毕竟这个策略接口HandlerMethodMappingNamingStrategy在Spring4.1后才有,这里的name是它生成出来的)
// 保存着name和HandlerMethod的对应关系(一个name可以有多个HandlerMethod)
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
// 这两个就不用解释了
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
// 读写锁~~~ 读写分离 提高启动效率
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
... // 提供一些查找方法,都不是线程安全的
// 读锁提供给外部访问,写锁自己放在内部即可~~~
public void acquireReadLock() {
this.readWriteLock.readLock().lock();
}
public void releaseReadLock() {
this.readWriteLock.readLock().unlock();
}
// 注册Mapping和handler 以及Method 此处上写锁保证线程安全~
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
// 此处注意:都是new HandlerMethod()了一个新的出来~~~~
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
// 同样的:一个URL Mapping只能对应一个Handler
// 这里可能会出现常见的一个异常信息:Ambiguous mapping. Cannot map XXX
assertUniqueMethodMapping(handlerMethod, mapping);
// 缓存Mapping和handlerMethod的关系
this.mappingLookup.put(mapping, handlerMethod);
// 保存url和RequestMappingInfo(mapping)对应关系
// 这里注意:多个url可能对应着同一个mappingInfo呢~ 毕竟@RequestMapping的url是可以写多个的~~~~
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
// 保存name和handlerMethod的关系 同样也是一对多
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
// 注册mapping和MappingRegistration的关系
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
// 释放锁
finally {
this.readWriteLock.writeLock().unlock();
}
}
// 相当于进行一次逆向操作~
public void unregister(T mapping) { ... }
...
}
这个注册中心,核心是保存了多个Map映射关系,相当于缓存下来。在请求过来时需要查找的时候,可以迅速定位到处理器
至此,我们就把 AbstractHandlerMethodMapping 的初始化流程看完了。
终于来到AbstractHandlerMethodMapping它对父类抽象方法:getHandlerInternal的实现如下:
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (CorsUtils.isPreFlightRequest(request)) {
for (Match match : matches) {
if (match.hasCorsConfig()) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
}
}
else {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.getHandlerMethod().getMethod();
Method m2 = secondBestMatch.getHandlerMethod().getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.getHandlerMethod();
}
else {
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}
}
这里就比较容易,通过 lookupHandlerMethod 找到对应的 HandlerMethod 返回即可,如果 lookupHandlerMethod 方法返回值不为 null,则通过 createWithResolvedBean 创建 HandlerMethod(主要是确认里边的 Bean 等)lookupHandlerMethod 方法也比较容易:
- 首先根据
lookupPath找到匹配条件directPathMatches,然后将获取到的匹配条件添加到 matches 中(不包含通配符的请求走这里)。 - 如果 matches 为空,说明根据 lookupPath 没有找到匹配条件,那么直接将所有匹配条件加入 matches 中(包含通配符的请求走这里)。
- 对 matches 进行排序,并选择排序后的第一个为最佳匹配项,如果前两个排序相同,则抛出异常。
Spring MVC请求URL带后缀匹配的情况,如/hello.json也能匹配/hello
RequestMappingInfoHandlerMapping 在处理http请求的时候, 如果 请求url 有后缀,如果找不到精确匹配的那个@RequestMapping方法。
那么,就把后缀去掉,然后.*去匹配,这样,一般都可以匹配,默认这个行为是被开启的。
比如有一个@RequestMapping("/rest"), 那么精确匹配的情况下, 只会匹配/rest请求。 但如果我前端发来一个 /rest.abcdef 这样的请求, 又没有配置 @RequestMapping("/rest.abcdef") 这样映射的情况下, 那么@RequestMapping("/rest") 就会生效。
究其原因咱们可以接着上面的分析,其实就到了PatternsRequestCondition这个类上,具体实现是它的匹配逻辑来决定的。
这里的代码有些简化,但是核心没变
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
...
@Override
@Nullable
public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
// patterns表示此MappingInfo可以匹配的值们。一般对应@RequestMapping注解上的patters数组的值
if (this.patterns.isEmpty()) {
return this;
}
// 拿到待匹配的值,比如此处为"/hello.json"
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
// 最主要就是这个方法了,它拿着这个lookupPath匹配~~~~
List<String> matches = getMatchingPatterns(lookupPath);
// 此处如果为empty,就返回null了~~~~
return (!matches.isEmpty() ? new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null);
}
public List<String> getMatchingPatterns(String lookupPath) {
List<String> matches = new ArrayList<>();
for (String pattern : this.patterns) {
// 最最最重点就是在getMatchingPattern()这个方法里~~~ 拿着lookupPath和pattern看它俩合拍不~
String match = getMatchingPattern(pattern, lookupPath);
if (match != null) {
matches.add(match);
}
}
// 解释一下为何匹配的可能是多个。因为url匹配上了,但是还有可能@RequestMapping的其余属性匹配不上啊,所以此处需要注意 是可能匹配上多个的 最终是唯一匹配就成~
if (matches.size() > 1) {
matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
}
return matches;
}
// // ===============url的真正匹配规则 非常重要~~~===============
// 注意这个方法的取名,上面是负数,这里是单数~~~~命名规范也是有艺术感的
@Nullable
private String getMatchingPattern(String pattern, String lookupPath) {
// 完全相等,那就不继续聊了~~~
if (pattern.equals(lookupPath)) {
return pattern;
}
// 注意了:useSuffixPatternMatch 这个属性就是我们最终要关闭后缀匹配的关键
// 这个值默外部给传的true(其实内部默认值是boolean类型为false)
if (this.useSuffixPatternMatch) {
// 这个意思是若useSuffixPatternMatch=true我们支持后缀匹配。我们还可以配置fileExtensions让只支持我们自定义的指定的后缀匹配,而不是下面最终的.*全部支持
if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
for (String extension : this.fileExtensions) {
if (this.pathMatcher.match(pattern + extension, lookupPath)) {
return pattern + extension;
}
}
}
// 若你没有配置指定后缀匹配,并且你的handler也没有.*这样匹配的,那就默认你的pattern就给你添加上后缀".*",表示匹配所有请求的url的后缀~~~
else {
boolean hasSuffix = pattern.indexOf('.') != -1;
if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
return pattern + ".*";
}
}
}
// 若匹配上了 直接返回此patter
if (this.pathMatcher.match(pattern, lookupPath)) {
return pattern;
}
// 这又是它支持的匹配规则。默认useTrailingSlashMatch它也是true
// 这就是为何我们的/hello/也能匹配上/hello的原因
// 从这可以看出,Spring MVC的宽容度是很高的,容错处理做得是非常不错的~~~~~~~
if (this.useTrailingSlashMatch) {
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
return pattern + "/";
}
}
return null;
}
}
分析了URL的匹配原因,现在肯定知道为何默认情况下”/hello.aaaa”或者”/hello.aaaa/“或者””/hello/””能匹配上我们/hello的原因了吧
Spring和SpringBoot中如何关闭此项功能呢?
为何要关闭的理由,上面其实已经说了。当我们涉及到严格的权限校验(强权限控制)的时候。特备是一些银行系统、资产系统等等,关闭后缀匹配事非常有必要的。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {
private boolean useSuffixPatternMatch = true;
private boolean useTrailingSlashMatch = true;
}
可以看到这两个属性值都直接冒泡到RequestMappingHandlerMapping这个实现类上来了,所以我们直接通过配置来改变它的默认行为就成。
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// 关闭后缀名匹配,关闭最后一个/匹配
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
configurer.setUseTrailingSlashMatch(false);
}
}
就这么一下,我们的URL就安全了,再也不能后缀名任意匹配了。
RequestMappingInfoHandlerMapping
提供匹配条件RequestMappingInfo的解析处理。
// @since 3.1 此处泛型为:RequestMappingInfo 用这个类来表示mapping映射关系、参数、条件等
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
// 专门处理Http的Options方法的HandlerMethod
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
} catch (NoSuchMethodException ex) {
throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex);
}
}
// 构造函数:给set了一个HandlerMethodMappingNamingStrategy
protected RequestMappingInfoHandlerMapping() {
setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
}
// 复写父类的抽象方法:获取mappings里面的patters们~~~
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatterns();
}
// 校验看看这个Mapping是否能匹配上这个request,若能匹配上就返回一个RequestMappingInfo
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}
...
}
它主要做的事就是确定了泛型类型为:RequestMappingInfo,然后很多方法都依托它来完成判定逻辑,比如上面三个@Override方法就是对父类抽象方法的实现。委托给RequestMappingInfo去实现的
而RequestMappingInfo的构建工作,Spring MVC理论上是可以允许有多种方案。鉴于Spring MVC给出的唯一实现类为RequestMappingHandlerMapping
RequestMappingHandlerMapping 唯一实现类
根据@RequestMapping注解生成RequestMappingInfo,同时提供isHandler实现。
直到这个具体实现类,才与具体的实现方式
@RequestMapping做了强绑定了
有了三层抽象的实现,其实留给本类需要实现的功能已经不是非常的多了~
核心代码,可能与源码有出入但是核心不变
// @since 3.1 Spring3.1才提供的这种注解扫描的方式的支持~~~ 它也实现了MatchableHandlerMapping分支的接口
// EmbeddedValueResolverAware接口:说明要支持解析Spring的表达式~
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
...
private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();
// 配置要应用于控制器方法的路径前缀
// @since 5.1:Spring5.1才出来的新特性,其实有时候还是很好的使的 下面给出使用的Demo
// 前缀用于enrich每个@RequestMapping方法的映射,至于匹不匹配由Predicate来决定 有种前缀分类的效果~~~~
// 推荐使用Spring5.1提供的类:org.springframework.web.method.HandlerTypePredicate
public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
}
// @since 5.1 注意pathPrefixes是只读的~~~因为上面Collections.unmodifiableMap了 有可能只是个空Map
public Map<String, Predicate<Class<?>>> getPathPrefixes() {
return this.pathPrefixes;
}
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
}
// If enabled a method mapped to "/users" also matches to "/users/".
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
this.useTrailingSlashMatch = useTrailingSlashMatch;
}
@Override
public void afterPropertiesSet() {
// 对RequestMappingInfo的配置进行初始化 赋值
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper()); // 设置urlPathHelper默认为UrlPathHelper.class
this.config.setPathMatcher(getPathMatcher()); //默认为AntPathMatcher,路径匹配校验器
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 是否支持后缀补充,默认为true
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); // 是否添加"/"后缀,默认为true
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); // 是否采用mediaType匹配模式,比如.json/.xml模式的匹配,默认为false
this.config.setContentNegotiationManager(getContentNegotiationManager()); //mediaType处理类:ContentNegotiationManager
// 此处 必须还是要调用父类的方法的
super.afterPropertiesSet();
}
...
// 判断该类,是否是一个handler(此处就体现出@Controller注解的特殊性了)
// 这也是为何我们的XXXController用@Bean申明是无效的原因(前提是类上木有@RequestMapping注解,否则也是阔仪的哦~~~)
// 因此我个人建议:为了普适性,类上的@RequestMapping也统一要求加上,即使你不写@Value也木关系,这样是最好的
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
// 还记得父类:AbstractHandlerMethodMapping#detectHandlerMethods的时候,回去该类里面找所有的指定的方法
// 而什么叫指定的呢?就是靠这个来判定方法是否符合条件的~~~~~
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
// 第一步:先拿到方法上的info
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
// 方法上有。在第二步:拿到类上的info
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
// 倘若类上面也有,那就combine把两者结合
// combile的逻辑基如下:
// names:name1+#+name2
// path:路径拼接起来作为全路径(容错了方法里没有/的情况)
// method、params、headers:取并集
// consumes、produces:以方法的为准,没有指定再取类上的
// custom:谁有取谁的。若都有:那就看custom具体实现的.combine方法去决定把 简单的说就是交给调用者了~~~
info = typeInfo.combine(info);
}
// 在Spring5.1之后还要处理这个前缀匹配~~~
// 根据这个类,去找看有没有前缀 getPathPrefix():entry.getValue().test(handlerType) = true算是hi匹配上了
// 备注:也支持${os.name}这样的语法拿值,可以把前缀也写在专门的配置文件里面~~~~
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
// RequestMappingInfo.paths(prefix) 相当于统一在前面加上这个前缀~
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
// 根据此方法/类,创建一个RequestMappingInfo
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
// 注意:此处使用的是findMergedAnnotation 这也就是为什么虽然@RequestMapping它并不具有继承的特性,但是你子类仍然有继承的效果的原因~~~~
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
// 请注意:这里进行了区分处理 如果是Class的话 如果是Method的话
// 这里返回的是一个condition 也就是看看要不要处理这个请求的条件~~~~
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
// 这个createRequestMappingInfo就是根据一个@RequestMapping以及一个condition创建一个
// 显然如果没有找到此注解,这里就返回null了,表面这个方法啥的就不是一个info~~~~
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
// 他俩都是返回的null。protected方法留给子类复写,子类可以据此自己定义一套自己的规则来限制匹配
// Provide a custom method-level request condition.
// 它相当于在Spring MVC默认的规则的基础上,用户还可以自定义条件进行处理~~~~
@Nullable
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return null;
}
@Nullable
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return null;
}
// 根据@RequestMapping 创建一个RequestMappingInfo
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
// 强大的地方在此处:path里竟然还支持/api/v1/${os.name}/hello 这样形式动态的获取值
// 也就是说URL还可以从配置文件里面读取 Spring考虑很周到啊~~~
// @GetMapping("/${os.name}/hello") // 支持从配置文件里读取此值 Windows 10
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
// 调用者自定义的条件~~~
if (customCondition != null) {
builder.customCondition(customCondition);
}
// 注意此处:把当前的config设置进去了~~~~
return builder.options(this.config).build();
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) { ... }
// 支持了@CrossOrigin注解 Spring4.2提供的注解
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { ... }
}
至此RequestMappingHandlerMapping的初始化完成了。像pathPrefixes这种配置,可以全局统一配置来控制每个Controller,如常用的/api/v1前缀
示例
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//configurer.setUseSuffixPatternMatch(false); //关闭后缀名匹配,关闭最后一个/匹配
//configurer.setUseTrailingSlashMatch(false);
// 这样HelloController上的方法自动就会有此前缀了,而别的controller上是不会有的
// 注意:这是Spring5.1后才支持的新特性
configurer.addPathPrefix("/api/v1", clazz -> clazz.isAssignableFrom(HelloController.class));
// 使用Spring提供的HandlerTypePredicate,更加的强大
HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackage("com.fsx");
//HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackageClass(HelloController.class);
//HandlerTypePredicate predicate = HandlerTypePredicate.forAssignableType(...);
//HandlerTypePredicate predicate = HandlerTypePredicate.forAnnotation(...);
//HandlerTypePredicate predicate = HandlerTypePredicate.builder()
// .basePackage()
// .basePackageClass()
// .build();
configurer.addPathPrefix("/api/v2", predicate);
}
}
若添加了两prefix都可以作用在某个Controller上,那么会按照放入的顺序(因为它是LinkedHashMap)以先匹配上的为准,可参考RequestMappingHandlerMapping#getPathPrefix方法
RequestMappingHandlerMapping 向容器中注册的时候,检测到实现了 InitializingBean接口,容器去执行afterPropertiesSet(),在afterPropertiesSet中完成Controller中完成方法的映射
以上就是Spring MVC在容器启动过程中,完成URL到Handler映射的所有内容
@RequestMapping属性详解
使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。
当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。
这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的。
// @since 2.5 用于将Web请求映射到具有灵活方法签名的请求处理类中的方法的注释 Both Spring MVC and `Spring WebFlux` support this annotation
// @Mapping这个注解是@since 3.0 但它目前还只有这个地方使用到了~~~ 我感觉是多余的
@Target({ElementType.METHOD, ElementType.TYPE}) // 能够用到类上和方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
//给这个Mapping取一个名字。若不填写,就用HandlerMethodMappingNamingStrategy去按规则生成
String name() default "";
// 路径 数组形式 可以写多个。 一般都是按照Ant风格进行书写~
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
// 请求方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
// 显然可以指定多个方法。如果不指定,表示适配所有方法类型~~
// 同时还有类似的枚举类:org.springframework.http.HttpMethod
RequestMethod[] method() default {};
// 指定request中必须包含某些参数值时,才让该方法处理
// 使用 params 元素,你可以让多个处理方法处理到同一个URL 的请求, 而这些请求的参数是不一样的
// 如:@RequestMapping(value = "/fetch", params = {"personId=10"} 和 @RequestMapping(value = "/fetch", params = {"personId=20"}
// 这两个方法都处理请求`/fetch`,但是参数不一样,进入的方法也不一样~~~~
// 支持!myParam和myParam!=myValue这种~~~
String[] params() default {};
// 指定request中必须包含某些指定的header值,才能让该方法处理请求
// @RequestMapping(value = "/head", headers = {"content-type=text/plain"}
String[] headers() default {};
// 指定处理请求request的**提交内容类型**(Content-Type),例如application/json、text/html等
// 相当于只有指定的这些Content-Type的才处理
// @RequestMapping(value = "/cons", consumes = {"application/json", "application/XML"}
// 不指定表示处理所有~~ 取值参见枚举类:org.springframework.http.MediaType
// 它可以使用!text/plain形如这样非的表达方式
String[] consumes() default {};
// 指定返回的内容类型,返回的内容类型必须是request请求头(Accept)中所包含的类型
// 仅当request请求头中的(Accept)类型中包含该指定类型才返回;
// 参见枚举类:org.springframework.http.MediaType
// 它可以使用!text/plain形如这样非的表达方式
String[] produces() default {};
}
Spring4.3之后提供了组合注解5枚:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
consumes 与 headers 区别
consumes produces params headers四个属性都是用来缩小请求范围。
consumes只能指定 content-Type 的内容类型,但是headers可以指定所有。
所以可以认为:headers是更为强大的(所有需要指定key和value嘛),而consumes和produces是专用的,头的key是固定的,所以只需要写value值即可,使用起来也更加的方便。
推荐一个类:org.springframework.http.HttpHeaders,它里面有常量:几乎所有的请求头的key,以及我们可以很方便的构建一个HttpHeader,平时可以作为参考使用
Spring MVC默认使用的HandlerMapping是什么?
Spring对这块的设计也是很灵活的,允许你自己配置,也允许你啥都不做使用Spring默认的配置。处理代码在:DispatcherServlet#initHandlerMappings
public class DispatcherServlet extends FrameworkServlet {
// 为此DispatcherServlet 初始化HandlerMappings
// 备注:DispatcherServlet是允许你有多个的~~~~
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
//detectAllHandlerMappings该属性默认为true,表示会去容器内找所有的HandlerMapping类型的定义信息
// 若想改为false,请调用它的setDetectAllHandlerMappings() 自行设置值(绝大部分情况下没啥必要)
if (this.detectAllHandlerMappings) {
// 这里注意:若你没有标注注解`@EnableWebMvc`,那么这里找的结果是空的
// 若你标注了此注解,这个注解就会默认向容器内注入两个HandlerMapping:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 多个的话 还需要进行一次排序~~~
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
// 不全部查找,那就只找一个名字为`handlerMapping`的HandlerMapping 实现精准控制
// 绝大多数情况下 我们并不需要这么做~
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
} catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// 若一个都没找到自定义的,回滚到Spring的兜底策略,它会想容器注册两个:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
// 输出trace日志:表示使用了兜底策略~
// 兜底策略配置文件:DispatcherServlet.properties
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
}
通过这段代码,我们能够很清晰的看到。绝大部分情况下,我们容器内会有这两个HandlerMapping Bean:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
换句话说,默认情况下@RequestMapping和BeanNameUrl的方式都是被支持的~
请注意:使用@EnableWebMvc和不使用它有一个非常非常重要的区别:
使用@EnableWebMvc原来是依托于这个WebMvcConfigurationSupport config类向容器中注入了对应的Bean,所以他们都是交给了Spring管理的(所以你可以@Autowired他们)
但是,但是,但是(重说三),若是走了Spring它自己去读取配置文件走默认值,它的Bean是没有交给Spring管理的,没有交给Spring管理的。它是这样创建的:context.getAutowireCapableBeanFactory().createBean(clazz) 它创建出来的Bean都不会交给Spring管理。
若你是纯Spring MVC环境,为确保万无一失,请开启SpringMVC:@EnableWebMvc
Spring MVC在启动时会扫描所有的@RequestMapping并封装成对应的RequestMapingInfo。
一个请求过来会与RequestMapingInfo进行逐个比较,找到最适合的那个RequestMapingInfo。
Spring MVC通过HandlerMapping建立起了Url Pattern和Handler的对应关系,这样任何一个URL请求过来时,就可以快速定位一个唯一的Handler,然后交给其进行处理了
当然这里面还有很多实现细节,其中还有一个非常重要的一块:HandlerAdapter
