转载

Spring MVC--请求映射与参数解析

本篇文章分析下Spring MVC如何映射一个URL地址到具体的 HandlerExecutionChain ,并转换request中的参数,最后执行所定位到的方法.

请求映射

Spring MVC八大组件之一的 HandlerMapping ,其主要负责请求与对应处理器的映射.按照一般项目可以把请求地址分为以下三种,来分析Spring MVC到底是如何处理的.

  1. 链接中无参数的, 比如 /api/v1/login/, /api/v1/*
  2. 链接中有参数的,比如 /api/{version}/login, /api/{version}/*
  3. 静态资源链接(包括一个具体的页面),比如/static/bootstrap.css

DispatcherServlet 中根据请求得到对应的处理链(包括具体执行的方法与拦截器)调的是 getHandler 方法,该方法对 HandlerMappings 做了一个循环处理,直到找到第一个符合的 HandlerExecutionChain 为止.

这里个人觉得可以做个优化,让每一个 HandlerMapping 持有一个 handlerCount 字段,每次选中的 HandlerMapping 处理成功后该 handlerCount 自增,然后对 this.handlerMappings 根据 handlerCount 排序,这样随着服务的运行,会使得这个循环的尽可能的用最少的次数找到最合适的处理器.

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
           // 对所有的Handler循环,直到找到能够处理的Handler
	for (HandlerMapping hm : this.handlerMappings) {
		if (logger.isTraceEnabled()) {
			logger.trace(
					"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
		}
		HandlerExecutionChain handler = hm.getHandler(request);
		if (handler != null) {
			return handler;
		}
	}
	return null;
}

根据上面的代码,寻找 HandlerExecutionChain 会委托给 HandlerMappinggetHandler 方法执行,那么接下来只需要分析 HandlerMapping 即可.对于 HandlerMapping 大概分为两种解析类型,如下图所示:

Spring MVC--请求映射与参数解析

AbstractHandlerMapping

先看最顶层的抽象类 AbstractHandlerMapping 中定义的模板方法,其中 getHandlerInternal 是抽象方法委托给子类实现.

@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
           // getHandlerInternal委托给子类实现查找handler的方法
	Object handler = getHandlerInternal(request);
	if (handler == null) {
		handler = getDefaultHandler();
	}
           // 子类找不到则返回null,没有对应的处理器
	if (handler == null) {
		return null;
	}
	// Bean name or resolved handler?
	if (handler instanceof String) {
		String handlerName = (String) handler;
		handler = getApplicationContext().getBean(handlerName);
	}
           // 构造handlerChain,主要是在hander中加入`HandlerInterceptor`拦截器.
	HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
	if (CorsUtils.isCorsRequest(request)) {
		CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
		CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
		CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
		executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
	}
	return executionChain;
}

如架构图所示 AbstractHandlerMapping 的子类有 AbstractHandlerMethodMapping , AbstractUrlHandlerMapping ,很明显一个是映射对应的处理方法,一个是映射具体的URL.

AbstractHandlerMethodMapping

该映射器主要是针对url-处理方法的映射关系,其内部持有 MappingRegistry 实例,该实例存放着所有的 @RequestMapping 所产生的映射关系,同时持有 ReentrantReadWriteLock ,也就是提供了并发访问的能力,其本身是读多写少的业务场景,因此读写锁是最合适的并发控制工具.

Spring MVC--请求映射与参数解析

当拿到请求链接后,变会转到 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod 方法中解析对应的处理方法 HandlerMethod ,如下注释:

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<Match>(); // 存放找到的结果
        // 因为URL是确定的,因此直接从映射关系中取,可以拿到一部分.
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
        // 找不到的话则直接全量匹配查找(这里是重点)
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}
        // 找到的结果可能有多个,因此需要筛选.
		if (!matches.isEmpty()) {
                // 排序规则为org.springframework.web.servlet.mvc.method.RequestMappingInfo#compareTo方法,感兴趣可以研究下
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			Collections.sort(matches, comparator);
			if (logger.isTraceEnabled()) {
				logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
						lookupPath + "] : " + matches);
			}
            // 排序后第一个是最佳匹配
			Match bestMatch = matches.get(0);
			if (matches.size() > 1) {
				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();
					throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
							request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
				}
			}
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.handlerMethod;
		}
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

上述查找流程会有哪些问题?

directPathMatches 匹配不到时,会造成全量的遍历,笔者公司一个20w行代码的项目全量匹配是要循环300次,每一个URL方法都要试试匹配,然后再排序,再筛选,并且随着请求量的增加循环次数也在增加,系统负载能力是下降趋势的.那么哪些操作造成全量匹配?

分析 directPathMatches 的来源,其是根据URL查找出对应的处理链,然后再挨个判断,换句话说非具体的URL就找不到对应的处理器链,从而造成全量匹配,对于Spring MVC来说是 @PathVariable 或者是 login/** 通配符形式会导致全量匹配.因为这两种情况下链接本身不是固定的,因此无法精确匹配,只能全量搜索查找.

如果项目大量使用了类似的写法,解决办法就是定制解析流程,可以参考达达的定制过程 SpringMVC RESTful 性能优化

// k:url  v:对应处理方法,可能多个
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<String, T>();
public List<T> getMappingsByUrl(String urlPath) {
			return this.urlLookup.get(urlPath);
}

AbstractUrlHandlerMapping

AbstractHandlerMethodMapping 的处理方式差不多,主要集中在 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#lookupHandler 方法,这里就不多做分析了,感兴趣的可以去阅读下.

参数解析转换

对于Spring MVC来说,找到对应的方法后能拿到这个方法需要的参数名称以及参数类型,位置顺序,参数是在Url路径(@PathVariable),还是在请求参数(@RequestParam),还是在playload(@RequestBody)等相关信息,然后要解决的问题是如何找到,以及如何转换?

如何找到?

Spring MVC中提供了 HandlerMethodArgumentResolver 对参数进行解析,其主要提供如下两个方法.

public interface HandlerMethodArgumentResolver {

    // 是否可解析判断
	boolean supportsParameter(MethodParameter parameter);
    // 具体解析方法
	Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}

其下是 AbstractNamedValueMethodArgumentResolver 这个抽象类,该类定义了整个解析以及转换流程

@Override
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
           .....省略一些代码
           // 解析,委托给具体的实现类
	Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
           .....省略一些代码
           // 转换流程,委托给WebDataBinder
	if (binderFactory != null) {
		WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
		try {
			arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
		}
               ...
	}
	handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
	return arg;
}

RequestHeaderMethodArgumentResolver 为例,其主要对应 @RequestHeader 的解析操作,那么是否支持只需要判断参数是否用 @RequestHeader 修饰,取值则直接从Hedaer中获取.至于转换则是有其上的抽象模板父类完成.对于我们来说是一个示例,你可以仿照 RequestHeaderMethodArgumentResolver 的实现来自定义自己的取值规则.

@Override
	public boolean supportsParameter(MethodParameter parameter) {
            // 根据参数是否有该注解判断是否支持
		return (parameter.hasParameterAnnotation(RequestHeader.class) &&
				!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()));
	}

	@Override
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
            // 解析操作直接从header中取出对应的值
		String[] headerValues = request.getHeaderValues(name);
		if (headerValues != null) {
			return (headerValues.length == 1 ? headerValues[0] : headerValues);
		}
		else {
			return null;
		}
	}

如何转换?

上述取值方法 resolveName 返回值为Object,调用 HadlerMethod 之前,需要转换为参数所需要的类型,在 AbstractNamedValueMethodArgumentResolver 可以看到如下的转换策略.

WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
       try {
           arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
       }

WebDataBinder最终会把转换委托给 org.springframework.core.convert.ConversionService 来实现,在 ConversionService 中包含有大量的 Converter ,也就是实际发生转换的地方.(转换这个委托太复杂了..这里直接略过跳到实际转换发生的地方)

Spring MVC--请求映射与参数解析

IntegerToEnumConverterFactory 为例,其实现的是数字到枚举类的转换,使用的是枚举类的 ordinal 属性.该类也是一个很好的示例,告诉我们如果自定义转换规则则可以直接实现 ConverterFactory 接口,然后根据类型可以很轻松的实现自定义转换逻辑.

final class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> {

	@Override
	public <T extends Enum> Converter<Integer, T> getConverter(Class<T> targetType) {
		return new IntegerToEnum(ConversionUtils.getEnumType(targetType));
	}

	private class IntegerToEnum<T extends Enum> implements Converter<Integer, T> {

		private final Class<T> enumType;

		public IntegerToEnum(Class<T> enumType) {
			this.enumType = enumType;
		}

		@Override
		public T convert(Integer source) {
                // 通过ordinal属性来定位到具体的枚举类.
			return this.enumType.getEnumConstants()[source];
		}
	}
}

总结

整个流程看下来实际上是有点懵逼的,总体感觉下来Spring MVC并不是一款对于性能追求极致的框架,而是一款对扩展性追求极致的框架,其提供了太多的hack入口,让你可以定制自己的解析逻辑或者扩展现有的策略.

而整个设计流程给我最大的感触就是变与不变的分离,就像圆规画圆,第一步永远是固定圆心,然后另一支轴可以任意扩展,无论是 DispatcherServlet 还是各种 AbstractXXXXX 的设计都是如此,不变的定义在上层,变化的转换成另一个接口沉淀到其他层,尽量降低其他层的复杂度,从而在整个系统上提供了很高的扩展性,希望对你有启发.

最后如有错误还请指出,以免误人子弟.

  • 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
  • 文章标题: Spring MVC--请求映射与参数解析
  • 文章链接: https://mrdear.cn/2018/04/07/framework/spring/Spring MVC--request_and_param/

Java学习记录--CAS操作分析

原文  https://mrdear.cn/2018/04/07/framework/spring/Spring MVC--request_and_param/
正文到此结束
Loading...