SpringMvc流程分析-1

SpringMvc流程分析-1

SpringMvc的大体的流程只是在网上大体的看了看,从没有对照源码来看看。本文就从源码开始,简单的分析SpringMvc的流程

这里的例子很简单了,就不举了,断点打到DispatchServlet的doDispatch

处理流程

大体的处理流程看 springMvc处理流程概述

补充说明

HandlerMapping的作用是什么?

HandlerMapping就一个方法,通过这个方法可以返回HandlerExecutionChain对象,这个对象会选择处理这个请求的handle和任何的interceptors。可能是通过RequestURL选择的,也有可能是Session的状态,总之,通过这个方法就会返回HandlerExecutionChain对象,表示处理该请求的类和拦截器。

此外,我们自己也可以实现这个接口,Spring默认实现了BeanNameUrlHandlerMappingRequestMappingHandlerMapping,如果没有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

在它和它的父类里面总共干了下面的几个事情:

  1. 在程序启动的时候从Bean工厂中获取到标注了@Controller注解的Bean和@RequestMapping的方法, 封装为RequestMappingInfo
  2. 父类提供了查找的基本的实现,增加了defaulthandle,和Interceptor封装为HandlerExecutionChain对象。
  3. RequestMappingInfo注册到MappingRegistry里面去,并且在注册的时候通过HandlerMethodMappingNamingStrategy来确定存的key
  4. 此外,可能存在一个请求会有多个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的使用。

关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值