阅读完需:约 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
方法完成注册。
上面这段代码里又涉及到两个方法:
getMappingForMethod
registerHandlerMethod
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