SpringMvc流程分析-1
SpringMvc的大体的流程只是在网上大体的看了看,从没有对照源码来看看。本文就从源码开始,简单的分析SpringMvc的流程
这里的例子很简单了,就不举了,断点打到DispatchServlet的doDispatch
处理流程
大体的处理流程看 springMvc处理流程概述
补充说明
HandlerMapping的作用是什么?
HandlerMapping就一个方法,通过这个方法可以返回HandlerExecutionChain
对象,这个对象会选择处理这个请求的handle和任何的interceptors。可能是通过RequestURL选择的,也有可能是Session的状态,总之,通过这个方法就会返回HandlerExecutionChain对象,表示处理该请求的类和拦截器。
此外,我们自己也可以实现这个接口,Spring默认实现了BeanNameUrlHandlerMapping
和RequestMappingHandlerMapping
,如果没有HandleMapping注册到ApplicationContext中的话,默认是前者。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
public interface HandlerMapping {
String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";
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";
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
在Springboot中,默认的HandleMapping如下图所示:
我们用的最多的是RequestMappingHandlerMapping
,
在它和它的父类里面总共干了下面的几个事情:
- 在程序启动的时候从Bean工厂中获取到标注了
@Controller
注解的Bean和@RequestMapping
的方法, 封装为RequestMappingInfo
- 父类提供了查找的基本的实现,增加了defaulthandle,和Interceptor封装为
HandlerExecutionChain
对象。 - 将
RequestMappingInfo
注册到MappingRegistry
里面去,并且在注册的时候通过HandlerMethodMappingNamingStrategy
来确定存的key - 此外,可能存在一个请求会有多个url匹配的情况(这里说的url),一个url可能对应多个handle,注意,这里只是说的是url,没有说方法的类型到底是post还是get,此外在
@RequestMapping
上面还有别的属性可以用,所以,下面就需要从多个匹配的情况中,获取最匹配的一个,如果有多个,就直接报错了。
从它里面就能获取到一个HandlerMethod
,它里面包含了处理的bean,和可以用的拦截器。对了,这个拦截器也有讲究,有MappedInterceptor
可以通过url来做正则匹配,匹配到了,才能起作用,普通的HandlerInterceptor
会给所有的请求都用。
HandlerAdapter的作用是什么?
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}
HandlerAdapter他是一个MVC的SPI,通过它就可以真正的处理请求,返回ModelAndView。利用它就可以将Handle和对应的处理逻辑分开。
有多少种Handle,就会有多少个和他们对应的HandlerAdapter
来处理。需要注意的是它的handle
方法和supports
方法参数名为handler的类型都是Object,把它作为object 的目的是为了别的框架可以很方便的整合进来,不用参考文档,直接一把梭哈。
它的作用就是很大的目的就是为了解耦,要是没有它的话,handle的查找,和调用就得写在一块。不是方便。
Controller是什么时候扫描到的。怎么处理的。
AbstractHandlerMethodMapping中实现的,AbstractHandlerMethodMapping是HandlerMapping
的实现类,并且实现了InitializingBean
接口,直接看afterPropertiesSet
方法.
会拿到所有的Bean,循环遍历,调用下面的方法,获取Bean,调用isHandler方法,它是AbstractHandlerMethodMapping
的抽象方法,留给子类去拓展。
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
// RequestMappingHandlerMapping#isHandler方法,判断bean是否有Controller注解或者requestMapping注解。看到这里的判断逻辑,突然觉得,是不是可以不用@Conterller注解,用@Component注解也可以,事实证明是可以的。
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
找到之后,就可以解析到了对应的注解里面的属性,封装为RequestMappingInfo,通过registerHandlerMethod
方法注册到mappingRegistry中。这个方法本身是没有可说的,有一个需要关注的点,就是对于@RequestMapping注解的解析操作。
在实际用的时候,类和方法上面都可以标注@RequestMapping注解,对于一个请求来说,处理的类肯定是唯一的,所以,这里就得将类和方法上的@RequestMapping注解做结合。
可以看AbstractHandlerMethodMapping#detectHandlerMethods(Object)
方法调用getMappingForMethod(Method,Class<?> )
方法,重点看RequestMappingHandlerMapping
的实现。
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//解析到方法上的 RequestMappingInfo
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//处理类上面的 RequestMappingInfo
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;
}
一个url多个匹配的情况要怎么处理,如果没有找到会怎么处理?
重点是在AbstractHandlerMethodMapping#lookupHandlerMethod
方法,先看代码。它的功能是通过lookPath找到唯一的一个HandleMethod对象。
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 通过url获取处理的信息(RequestMappingInfo)这里只是一个请求的url,一个请求的url可能对应多个请求方式,比如get或者post,此外还有别的属性。
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 添加到matches的数组里面,为啥是数组,因为这里有多个匹配。
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) { // 如果是空的,就扩大搜索范围,全局搜索,
addMatchingMappings(this.mappingRegistry.getMappings().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 (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request); //将抽取的所有的信息都放在request的属性里面了,这里面做了从path或者media中提取到的具体的信息,放在request的属性中了
return bestMatch.handlerMethod;
}
else {
//处理没有找到,直接返回null,子类可以重写这个方法来做操作, RequestMappingInfoHandlerMapping 重写了他,增加了一些异常的细分的判断
// 比如,他是有对应处理的handle,只是参数不匹配,或者一些原因。
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
这里的代理好理解着,有几个地方得说说
-
为什么一个请求路径可能对应多个
RequestMappingInfo
?因为在
AbstractHandlerMethodMapping的MappingRegistry的urlLookup属性中,key是请求的路径,value是一个泛型。
可以看
register(T,Object,Method)
方法,在往urlLookup属性中注册的逻辑。 -
匹配的逻辑是什么?
对应的逻辑在
RequestMappingInfo#getMatchingCondition(HttpServletRequest)
方法里面。RequestMappingInfo代表了RequestMapping的信息,直接对应的就是@RequestMapping注解,他是在解析@Controller注解的时候创建的,相关的代码在
AbstractHandlerMethodMapping#detectHandlerMethods
中,点击getMappingForMethod
方法,看RequestMappingHandlerMapping
的实现,从createRequestMappingInfo
中可以看到通过RequestMappingInfo.Builder
来构建RequestMappingInfo
。在它里面聚合了下图所示的一些Condition,这些不同的Condition代表@RequestMapping注解中不同的属性值,在做匹配的时候,也就是通过路径查到到符合条件的之后,会通过他们来做匹配,只要有一个不匹配,就返回null,表示没有匹配到。具体的可以看看
RequestCondition
接口的实现类。
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
在所有的匹配都符合之后,创建RequestMappingInfo对象返回。
再找到两个匹配项之后,做比较的逻辑是什么?
找到多个匹配项之后,会比较,获取优先级最高的返回。这里的比较是通过Comparator
来做的,看看它的Comparator长什么样子
private class MatchComparator implements Comparator<Match> {
private final Comparator<T> comparator;
public MatchComparator(Comparator<T> comparator) {
this.comparator = comparator;
}
@Override
public int compare(Match match1, Match match2) {
return this.comparator.compare(match1.mapping, match2.mapping);
}
}
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}
MatchComparators实现了Comparator接口,compare方法里面用的Comparator是getMappingComparator方法返回值,最终的比较逻辑是RequestMappingInfo#compareTo(RequestMappingInfo , HttpServletRequest )
方法。又走到了RequestMappingInfo对象里面,代码如下所示.
还是交给了RequestCondition来做比价,注意,这里的顺序是和上一步匹配的顺序一致的,只要有一个不相等,就直接返回。
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result;
// Automatic vs explicit HTTP HEAD mapping
if (HttpMethod.HEAD.matches(request.getMethod())) {
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
}
result = getActivePatternsCondition().compareTo(other.getActivePatternsCondition(), request);
if (result != 0) {
return result;
}
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0) {
return result;
}
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0) {
return result;
}
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0) {
return result;
}
result = this.producesCondition.compareTo(other.getProducesCondition(), request);
if (result != 0) {
return result;
}
// Implicit (no method) vs explicit HTTP method mappings
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
if (result != 0) {
return result;
}
return 0;
}
假如说,通过比较之后,还是有两个及以上的项之后,会直接报错,错误信息为
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
没有找到对应处理该请求的handle怎么办?
对应的处理逻辑在 AbstractHandlerMethodMapping#handleNoMatch
中,得看它子类的实现(RequestMappingInfoHandlerMapping
)。在调用的时候会将所有已经注册的RequestMappingInfo传递进来,居然是一个Set,就得看看它的hashcode方法重写了没有,可以看RequestMappingInfo#calculateHashCode
方法。
在没有找到该请求url对应的handle的类型,就会到这里来,通过PartialMatchHelper
来找出路径匹配的,注意这里是路径匹配的,针对不同的类型做不同的处理,比如方法是否不支持等等。
如果压根url都没有,还是会返回null的。一直会返回到HandlerMapping的查找处理。表示该HandleMapping不支持。转而去找下一个HandlerMapping,遍历完了之后,都不支持,看标志位要不要报错,并且设置状态码为404.
对于404的补充说明:
在web.xml配置时代,可以通过xml来配置对应的处理,如下所示
<error-page> <error-code>404</error-code> <location>/WEB-INF/jsp/err_404.jsp</location> </error-page> <error-page> <error-code>404</error-code> <location>/WEB-INF/jsp/err_500.jsp</location> </error-page>
在Springboot时代,tomcat是嵌入在程序中的,那么这个配置是怎么做的?还有没有这个配置。
1. 是在
TomcatServletWebServerFactory#configureContext方法里面
,配置error相关的逻辑。在Springboot中配置的是/error
,并且还配置了一个BasicErrorController
来处理这个错误请求。
protected HandlerMethod handleNoMatch(
Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
PartialMatchHelper helper = new PartialMatchHelper(infos, request);
if (helper.isEmpty()) {
return null;
}
if (helper.hasMethodsMismatch()) {
Set<String> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
}
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
if (helper.hasProducesMismatch()) {
Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
}
if (helper.hasParamsMismatch()) {
List<String[]> conditions = helper.getParamConditions();
throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
}
return null;
}
SpringBoot中经典的错误页面是怎么处理的?
就这种很经典的错误页面。他是怎么被配置到Tomcat的。又是怎么来处理的。
之前说过了,错误的处理在web.xml时代是配置在配置文件中的,在Springboot时代,Tomcat服务器是嵌入到程序里面的,所以,肯定是在创建服务器的时候配置这些信息的。
创建服务器并且配置的具体逻辑在(对于TomcatServlet来说)TomcatServletWebServerFactory#configureContext
,在SpringBoot是按照下面的这种思想来创建的配置的
首先配置文件用类表示(ErrorProperties
),提供了一个接口名字叫做ErrorPageRegistry
表示errorPage的持有者,用ErrorPageRegistrar
表示注册器,可以通过注册器向ErrorPageRegistry
中注册,但是这里又一个问题,什么时候注册呢?回想Spring的生命周期的各个环节,只有在postProcessBeforeInitialization
方法有点适合,所以,SpringBoot自己也提供了
ErrorPageRegistrarBeanPostProcessor
来解析注册。
TomcatReactiveWebServerFactory的父类实现了ErrorPageRegistrar
接口,所以在启动Tomcat的时候可以获取到所有的ErrorPage对象,从而配置进来。此外Springboot的自动配置类,还帮我们配置了一个BasicErrorController
,用来处理/error请求。具体可以看ErrorMvcAutoConfiguration
(默认的配置信息)
当一个请求没有找到对应的handle的时候,就会将它的ResponseStatus设置为404,Tomcat看到这个标志位,就会请求配置的路径,请求就会再次到Spring中,注意,这个请求操作还是要走一遍标准的Spring mvc 处理请求的流程的。
先看看BasicErrorController
长什么样子
这俩本体都差不多,不过,errorHtml返回的是html,error返回的是json类型的。
默认的DefaultErrorAttributes
在返回值里面提供了如下的几个属性。
本文还没有说完,下一篇的内容是SpringMvc Conteroller入参的处理, 方法返回值的处理(ModelAndView,@ResponseBody等等),不同视图的处理,拦截器的加载和调用,Filter的使用。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。