【Spring】抽丝剥茧SpringMVC-RequestMappingHandlerMapping

本文详细阐述了SpringMVC中的RequestMappingHandlerMapping的作用、装配过程、核心逻辑及配置方法,包括默认策略、web.xml配置、SpringBoot启动等。

本文源码基于SpringMVC 5.2.7版本

 

    在《抽丝剥茧DispatcherServlet》一文中,提到了HandlerMapping。HandlerMapping不是具体的Handler,它的作用是找到与当前Request匹配的Handler。可以看出HandlerMapping是DispatcherServlet能够正常工作的一个重要组件。接下来详细介绍HandlerMapping及其重要实现类RequestMappingHandlerMapping。关于HandlerMapping,我们需要了解:

DispatcherServlet如何装配HandlerMapping

    在《抽丝剥茧DispatcherServlet》中介绍了DispatcherServlet初始化过程,简单来说就是系统在启动的时候会调用DispatcherServlet的initStrategies方法,该方法依次装配DispatcherServlet需要的组件,其中就包括HandlerMapping。具体的源码位于org.springframework.web.servlet.DispatcherServlet#initHandlerMappings。装配HandlerMapping的过程如下:

  1. 如果"detectAllHandlerMappings"打开,则从IOC容器中获取所有类型为HandlerMapping的实例;否则进入2
  2. 从IOC容器中获取name为"handlerMapping"的实例;
  3. 如果步骤1、2之后已经有HandlerMapping则装配过程结束,否则进入4;
  4. 通过DispatcherServlet默认装配策略中创建HandlerMapping实例,并装配给DispatcherServlet,装配过程结束。

默认情况“detectAllHandlerMappings”是打开的,也就是说默认情况是从IOC容器中找到所有HandlerMapping的Bean。

DispatcherServlet默认装配策略

    如果开发者没有配置HandlerMapping的Bean,那么DispatcherServlet就会执行默认装配策略。DispatcherServlet初始化对象时候就是从文件"DispatcherServlet.properties"中获取各组件默认的类名。DispatcherServlet.properties文件位于spring-webmvc.jar包,与DispatcherServlet.class在同一个目录。

public class DispatcherServlet extends FrameworkServlet {
... ...  

   static {
		// Load default strategy implementations from properties file.
		// This is currently strictly internal and not meant to be customized
		// by application developers.
		try {
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
		}
	}
... ...
}

DispatcherServlet.properties内入如下,就是一个个key-value,key是DispatcherServlet所需要的组件的接口全名称,value是具体的实现类(多个用“,”隔开)

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

所谓的默认装配策略就是从DispatcherServlet.properties读取实际的实现类,然后实例化对象出来。

RequestMappingHandlerMapping结构

RequestMappingHandlerMapping是HandlerMapping的一个重要实现类,在SpringMVC中大部分请求都是通过RequestMappingHandlerMapping找到对应Handler的。RequestMappingHandlerMapping的类图如下:

RequestMappingHandlerMapping主要相关接口、类的属性、方法如下

HandlerMapping接口:

  • getHandler(HttpServletRequest request):根据request获取对应的Handler。

AbstractHandlerMapping抽象类:

  • urlPathHelper:url路径解析的工具类,类型是org.springframework.web.util.UrlPathHelper。
  • pathMatcher:路径匹配的工具类,类型是org.springframework.util.AntPathMatcher。
  • interceptors:额外的拦截器,元素类型是Object,RequestMappingHandlerMapping初始化的时候会将其包装并添加到adaptedInterceptors。
  • adaptedInterceptors:SpringMVC拦截器,是一个List,元素类型是org.springframework.web.servlet.HandlerInterceptor。元素来自2部分,其一,容器中配置的HandlerInterceptor类型的Bean,其二是上面介绍的interceptors。

AbstractHandlerMethodMapping抽象类:

  • mappingRegistry:保存着Handler,类型是org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry。

MappingRegistry类:

  • mappingLookup:Map,key是RequestMappingInfo对象, value是对应Handler。
  • urlLookup:Map,key是url,value是RequestMappingInfo对象。
  • register:成员方法,向MappingRegistry注册Handler。

RequestMappingInfo类:

RequestMappingInfo里面就是一系列条件,这些条件负责匹配request是否匹配该RequestMappingInfo。

  • patternsCondition:路径模式条件,与@RequestMapping注解的path对应,类型是org.springframework.web.servlet.mvc.condition.PatternsRequestCondition。
  • methodsCondition:请求方法条件,与@RequestMapping注解的method对应,类型是org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition。
  •  paramsCondition:参数参数条件,与@RequestMapping注解的params对应,类型是org.springframework.web.servlet.mvc.condition.ParamsRequestCondition。
  • headersCondition:请求头条件,与@RequestMapping注解的headers
    对应,类型是org.springframework.web.servlet.mvc.condition.HeadersRequestCondition。 
  • consumesCondition:请求头Content-type条件即请求内容类型条件,与@RequestMapping注解的consumes对应,类型是org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition。
  • producesCondition:请求头Accept条件,与@RequestMapping注解的produces
    对应,类型是org.springframework.web.servlet.mvc.condition.ProducesRequestCondition。
  • customConditionHolder:自定义的一些条件,一般没有,需要自定义实现,类型是org.springframework.web.servlet.mvc.condition.RequestConditionHolder。

RequestMappingHandlerMapping初始化

    前面介绍了RequestMappingHandlerMapping相关主要接口和类的属性,那么这些属性是在什么时候赋值的,尤其Handler是什么时候注册到mappingRegistry的。

    从RequestMappingHandlerMapping的类图还可以看到它实现类接口org.springframework.beans.factory.InitializingBean。Spring IOC机制在创建Bean的时候调用该接口的afterPropertiesSet的方法。在RequestMappingHandlerMapping实现中调用initHandlerMethods方法。

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
... ...

   protected void initHandlerMethods() {
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

... ...
}

很明显,遍历候选的Bean然后处理,我们看下候选的Bean有哪些?

protected String[] getCandidateBeanNames() {
		return (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
				obtainApplicationContext().getBeanNamesForType(Object.class));
	}

过分,Object.class的Bean都是候选的Bean,那不就是all? 继续往下看,它对候选的Bean做了什么处理

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);
		}
	}
protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

 过滤有注解@Controller或者注解@RequestMapping的Bean,探测该Bean里面的可以处理Http请求的方法,即被注解@RequestMapping所注解的方法。对于符合条件的方法,会创建一个RequestMappingInfo对象。RequestMappingInfo对象包含了一些条件,这些条件决定了requst是否满足。如果所有条件都满足,则说明该RequestMappingInfo对应的方法将处理该request。源码如下

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {

... ...

    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
		//笔者注:先根据menthod创建RequestMappingInfo
        RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
            //笔者注:根据class创建RequestMappingInfo,然后合并RequestMappingInfo
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
				info = typeInfo.combine(info);
			}
            //笔者注:如果配置了路径前缀(一般不会配置),同样需要合并RequestMappingInfo
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				info = 
    RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
			}
		}
		return info;
	}

    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
		RequestCondition<?> condition = (element instanceof Class ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}

    protected RequestMappingInfo createRequestMappingInfo(
			RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) 
    {

		RequestMappingInfo.Builder builder = RequestMappingInfo
				.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);
		}
		return builder.options(this.config).build();
	}

... ...
}
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
... ...

public RequestMappingInfo build() {

			PatternsRequestCondition patternsCondition = ObjectUtils.isEmpty(this.paths) ? null :
					new PatternsRequestCondition(
							this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
							this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
							this.options.getFileExtensions());

			ContentNegotiationManager manager = this.options.getContentNegotiationManager();

			return new RequestMappingInfo(this.mappingName, patternsCondition,
					ObjectUtils.isEmpty(this.methods) ?
							null : new RequestMethodsRequestCondition(this.methods),
					ObjectUtils.isEmpty(this.params) ?
							null : new ParamsRequestCondition(this.params),
					ObjectUtils.isEmpty(this.headers) ?
							null : new HeadersRequestCondition(this.headers),
					ObjectUtils.isEmpty(this.consumes) && !this.hasContentType ?
							null : new ConsumesRequestCondition(this.consumes, this.headers),
					ObjectUtils.isEmpty(this.produces) && !this.hasAccept ?
							null : new ProducesRequestCondition(this.produces, this.headers, manager),
					this.customCondition);
		}
... ...
}

创建RequestMappingInfo的工作交给RequestMappingInfo.Builder完成。框架将扫描到的@RequstMapping注解中的参数值赋予Builder,Builder则去创建RequestMappingInfo对象。随后框架向MappingRegistry注册该RequestMappingInfo,注册的源码位于org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register,很简单就是往Map里面添加元素。值得注意的是注册的处理方法实际上是HandlerMethod对象。

public HandlerMethod(Object bean, Method method) {
		Assert.notNull(bean, "Bean is required");
		Assert.notNull(method, "Method is required");
		this.bean = bean;
		this.beanFactory = null;
		this.beanType = ClassUtils.getUserClass(bean);
		this.method = method;
		this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
		this.parameters = initMethodParameters();
		evaluateResponseStatus();
		this.description = initDescription(this.beanType, this.method);
	}

bridgedMethod是方法的原始方法,如果method是桥接方法(isBridge()),则这里努力找到原始方法。桥接方法是java编译器配合类型擦除而自动生成的方法。这里不做说明,大家去看看java的桥接方法的来龙去脉。

RequestMappingHandlerMapping核心逻辑

   RequestMappingHandlerMapping的作用就是从注册的Handler,找到request对应的Handler。在RequestMappingHandlerMapping初始化的时候,检测出了应用所有的@RequestMapping注解所修饰的方法,将其注册到注册中心,并根据@RequestMapping的配置创建一系列的RequestCondition对象。RequestMappingHandlerMapping匹配Handler的逻辑如下:

 

RequstMappingInfo的条件匹配过程是依次匹配methods条件、params条件、请求头条件等等,源码如下:

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
... ...

    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());
	}

... ...
}

RequstMappingInfo中的条件都是RequestCondition的实现类,RequestCondition接口有3个方法,各实现类必须实现。

  • combine:合并两个同类的RequestCondition对象。
  • getMatchingCondition:判断request是否匹配该条件。
  • compareTo:比较两个同类的RequestCondition对象。

下面从构造实例、匹配两点依次分析各个实现类。

RequestMethodsRequestCondition

RequestMethodsRequestCondition用来匹配请求的方式,常见的方式有get、post、head、put、delete、options等等

构造实例

构造RequestMethodsRequestCondition是在创建RequestMappingInfo的时候创建的,我们看下org.springframework.web.servlet.mvc.method.RequestMappingInfo.DefaultBuilder#build。RequestMethodsRequestCondition的关键成员变量如下:

  • methods:实例变量,RequestMethod集合,值来源于@RequestMapping的method
  • requestMethodConditionCache:静态变量,预先给HTTP协议支持的每个方式创建的RequestMethodsRequestCondition对象,这个对象的methods只包含一个方式

匹配逻辑

跨域时浏览器先发出一个预检请求(OPTIONS方式),预检通过才发出实际请求。预检请求在请求头"Access-Control-Request-Method"中指定实际的请求方式,所以RequestMethodsRequestCondition匹配逻辑兼容这一点。源码在org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition#getMatchingCondition。

  • 如果是跨域预请求(即OPTIONS方式)
    • 如果RequestMethodsRequestCondition的methods为空则返回匹配自己
    • 否则判断this.methods中是否包含请求头"Access-Control-Request-Method"指定的方式,包含则从requestMethodConditionCache返回该方式对应的对象,否则返回null表示未匹配
  • 不是OPTIONS请求
    • 如果RequestMethodsRequestCondition的methods为空则返回匹配自己
    • 否则判断this.methods中是否包含该方式,包含则从requestMethodConditionCache返回该方式对应的对象,否则返回null表示未匹配

ParamsRequestCondition

ParamsRequestCondition是用来匹配请求参数是否符合@RequestMapping指定的要求,例如不能包含某个参数key或者某个参数key值必须等于某个值。

构造实例

ParamsRequestCondition的构造逻辑与RequestMethodsRequestCondition一样,也是随着RequestMappingInfo创建。其关键成员变量如下:

  • expressions:ParamExpression类型的集合,@RequestMapping注解的params属性的每个值将解析成ParamExpression类型的对象

ParamExpression是org.springframework.web.servlet.mvc.condition.AbstractNameValueExpression的实现类,AbstractNameValueExpression支持正反逻辑的表达式

形式描述备注
!myName请求参数中不包含myName或myName.x或myName.y

.x、.y是前端利用图片提交表单,参数中会带鼠标的点击位置

<input type="image" src="xxx.gif" οnclick="return dosubmit();">

myName请求参数中包含myName或myName.x或myName.y
myName=myValue请求参数包含myName或myName.x或myName.y且值等于myValue
myName!=myValue请求参数包含myName或myName.x或myName.y且值不等于myValue

 

 

 

 

 

 

匹配逻辑

ParamsRequestCondition匹配逻辑非常简单,依次匹配参数表达式集合,有1个不匹配则返回null,否则返回自身。源码在org.springframework.web.servlet.mvc.condition.ParamsRequestCondition#getMatchingCondition。

HeadersRequestCondition

HeadersRequestCondition用来匹配请求头是否符合@RequestMapping中指定的请求头表达式,例如不能包含某个请求头或者某个请求头值必须等于某个值。

构造实例

HeadersRequestCondition的关键成员变量如下:

  • expressions:HeaderExpression类型集合,@RequestMapping注解的headers属性的每个值将解析成HeaderExpression类型的对象
  • PRE_FLIGHT_MATCH:静态变量,expressions集合为空的HeadersRequestCondition对象,预检请求时返回

HeaderExpression也是是org.springframework.web.servlet.mvc.condition.AbstractNameValueExpression的实现类,其逻辑与ParamExpression基本一致,参见ParamExpression的匹配表格。区别在于HeaderExpression不需要匹配myName.x和myName.y。

匹配逻辑

HeadersRequestCondition匹配逻辑非常简单,源码在org.springframework.web.servlet.mvc.condition.HeadersRequestCondition#getMatchingCondition

  • 如果是预检请求(OPTIONS)则直接返回静态变量PRE_FLIGHT_MATCH
  • 否则依次匹配参数表达式集合,有1个不匹配则返回null,否则返回自身

ConsumesRequestCondition

ConsumesRequestCondition用来判断请求头Content-Type所表示media是否符合@RequestMapping指定的media。

构造实例

ConsumesRequestCondition关键成员变量如下:   

  • expressions:ConsumeMediaTypeExpression类型集合,@RequestMapping属性consumes的每个值将解析为ConsumeMediaTypeExpression对象
  • bodyRequired:bool类型,表示请求的body是否必须有,请求头"Content-Length"不为"0"或者请求头"Transfer-Encoding"不为空的请求视为body不为空
  • EMPTY_CONDITION:静态变量,表示没有配置Content-Type匹配条件即匹配所有

ConsumeMediaTypeExpression是org.springframework.web.servlet.mvc.condition.AbstractMediaTypeExpression实现类,支持正反表达式

形式描述备注
*/*匹配所有MediaType 
!application/json不匹配application/json 

 

 

 

所有支持的MediaType

        ALL = new MediaType("*", "*");
		APPLICATION_ATOM_XML = new MediaType("application", "atom+xml");
		APPLICATION_CBOR = new MediaType("application", "cbor");
		APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded");
		APPLICATION_JSON = new MediaType("application", "json");
		APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
		APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");
		APPLICATION_PDF = new MediaType("application", "pdf");
		APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");
		APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem+json", StandardCharsets.UTF_8);
		APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml");
		APPLICATION_RSS_XML = new MediaType("application", "rss+xml");
		APPLICATION_STREAM_JSON = new MediaType("application", "stream+json");
		APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml");
		APPLICATION_XML = new MediaType("application", "xml");
		IMAGE_GIF = new MediaType("image", "gif");
		IMAGE_JPEG = new MediaType("image", "jpeg");
		IMAGE_PNG = new MediaType("image", "png");
		MULTIPART_FORM_DATA = new MediaType("multipart", "form-data");
		MULTIPART_MIXED = new MediaType("multipart", "mixed");
		MULTIPART_RELATED = new MediaType("multipart", "related");
		TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
		TEXT_HTML = new MediaType("text", "html");
		TEXT_MARKDOWN = new MediaType("text", "markdown");
		TEXT_PLAIN = new MediaType("text", "plain");
		TEXT_XML = new MediaType("text", "xml");

匹配逻辑

匹配逻辑源码在org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition#getMatchingCondition 。

  • 1. 如果是预检请求则直接返回EMPTY_CONDITION,表示匹配
  • 2. 如果expresstions为空,则返回自身,表示匹配
  • 3. 如果bodyRequired为true,且请求body不为空则返回EMPTY_CONDITION,表示匹配。请求头"Content-Length"不为"0"或者请求头"Transfer-Encoding"不为空的请求视为body不为空
  • 4. 解析请求头"Content-Type"表示的Media
  • 5. 依次遍历expressions集合中元素(用expression表示)
    • 5.1 如果expression匹配"Content-Type"表示的Media,则该expression作为候选加入到候选集
  • 6. 如果候选集合不为空,则新建ConsumesRequestCondition对象返回,并将候选集赋予新对象的expressions集合;如果候选集合为空,则返回null表示不匹配

ProducesRequestCondition

ProducesRequestCondition用来判断请求头"Accept"表示的Media是否符合@RequestMapping指定的Media。

构造实例

ProducesRequestCondition关键成员变量如下:

  • expressions:ProduceMediaTypeExpression类型集合,@RequestMapping属性produces的每个值将解析为ConsumeMediaTypeExpression对象
  • contentNegotiationManager:ContentNegotiationManager类型对象,用来解析请求可接受的Media
  • DEFAULT_CONTENT_NEGOTIATION_MANAGER:静态变量,默认的ContentNegotiationManager对象
  • EMPTY_CONDITION:静态变量,ProducesRequestCondition对象,表示没有配置Accept匹配条件即匹配所有
  • MEDIA_TYPE_ALL_LIST:静态变量,ProduceMediaTypeExpression集合,包含一个匹配所有的Medai(*/*)

ProduceMediaTypeExpression也是org.springframework.web.servlet.mvc.condition.AbstractMediaTypeExpression实现类,支持正反表达式。它与ConsumeMediaTypeExpression不同点在于

  • ProduceMediaTypeExpression需要匹配MediaType的paramters
  • ProduceMediaTypeExpression匹配是可接受Media列表与@RequestMapping.produces的双向兼容,任何一方兼容另一方都可;而ConsumeMediaTypeExpression则是@RequestMapping.consumes包含"Content-Type"

ProduceMediaTypeExpression匹配的源码在org.springframework.web.servlet.mvc.condition.ProducesRequestCondition.ProduceMediaTypeExpression#match。

匹配逻辑

  • 1. 如果是预检请求则直接返回EMPTY_CONDITION,表示匹配
  • 2. 如果expresstions为空,则返回自身,表示匹配
  • 3. 解析请求可接受的Media列表,解析逻辑源码在org.springframework.web.accept.ContentNegotiationManager#resolveMediaTypes
  • 4. 依次遍历expressions集合中元素(用expression表示)
    • 4.1 如果expression匹配可接受的Media列表,则该expression作为候选加入到候选集合
  • 5. 如果候选集合不为空,则新建ProducesRequestCondition对象返回并将候选集合赋予新对象的expressions集合;
  • 6. 如果候选集合为空但是可接受列表中包含"*/*",则返回EMPTY_CONDITION,表示匹配
  • 7. 否则返回null,表示不匹配

PatternsRequestCondition

PatternsRequestCondition用来匹配请求路径是否满足@RequestMapping注解指定的路径模式。路径模式是ant风格的模式。

构造实例

PatternsRequestCondition的关键成员变量如下:

  • patterns:String集合,表示该条件支持的ant风格的路径模式,值来源于@RequestMapping的path属性
  • pathHelper:UrlPathHelper对象,提供从request获取请求路径的工具方法
  • pathMatcher:PathMatcher对象,默认实现类是org.springframework.util.AntPathMatcher,将请求路径与路径模式进行匹配的工具类
  • useSuffixPatternMatch:bool类型,表示后缀是否参与匹配
  • useTrailingSlashMatch:bool类型,表示是否需要在模式末尾加上"/"参与匹配
  • fileExtensions:String集合,表示支持的后缀类型,与useSuffixPatternMatch配合使用
  • EMPTY_PATH_PATTERN:静态变量,包含一个空字符串("")的路径模式

匹配逻辑

PatternsRequestCondition的匹配就是将ant风格的patterns与请求路径进行匹配。ant风格模式的匹配规则如下

  • ?匹配1个任意字符。
  • *  匹配0个或多个任意字符。
  • ** 匹配0个或多个“目录” 。
  • {变量名} 匹配路径变量, pattern上"{"、"}"之间的字符作为路径变量名,URL路径上匹配"{"、"}"之间的字符作为路径变量的值。例如pattern是/user/{userId},请求的URL是/user/12345,那么路径变量名就是userId,值就是12345。
  • {变量名:java正则} 匹配符合指定正则的路径变量,且路径上变量值要符合冒号(:)后的java正则。例如pattern是/user/{userId:}。例如pattern是/user/{userId:\\d+},要求匹配的这个pattern的请求路径中userId必须是数字,请求的URL是/user/12345,那么路径变量名就是userId,值就是12345。但是如果请求的路径是/user/123abc就不会命中这个pattern是123abc不是纯数字。

PatternsRequestCondition匹配逻辑大致分以下步骤,org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingCondition

  • 1. 通过UrlPathHelper获取请求路径(用path表示)
  • 2. 依次遍历路径模式集合的元素(用pattern表示)
    • 2.1 将path和pattern分别用 "/"分割成数组path目录数组和pattern目录数组
    • 2.2 依次从path目录数组和pattern目录数组取出元素
      • 2.2.1 将pattern数组元素(ant风格模式)变换为java正则表达式
      • 2.2.2 判断path数组元素是否符合正则表达式
      • 2.2.3 如果path目录数组中每个元素都能按顺序匹配到pattern目录数组(注意path目录数组和pattern目录数组元素不是一一对应,因为**匹配0个或多个目录),则说明该pattern符合条件,作为候选pattern加入候选集合
    • 2.3 返回候选集合
  • 3. 候选集合不为空,则表示匹配,则新建PatternsRequestCondition对象作为返回值并将候选集合赋予新的PatternsRequestCondition对象;如果集合为空,则返回null表示为匹配路径条件

ant规则匹配的源码org.springframework.util.AntPathMatcher#doMatch

在ant规则匹配中比较重要的一环是将ant风格pattern转换为java的正则表达式,那么如何将ant风格pattern转换为java的正则表达式?其逻辑如下,源码在org.springframework.util.AntPathMatcher.AntPathStringMatcher#AntPathStringMatcher(java.lang.String, boolean)

  • 如果pattern中有字符"?",用字符"."替代
  • 如果pattern中有字符"*",用字符串".*"替代
  • 如果字符中有形如"{变量名}"的格式,用字符串"(.*)"替代 。与字符串".*"不同,"(.*)"在java正则匹配的时候能从group中取到命中的字符串,可以作为变量名对应的值
  • 如果字符中有形如"{变量名:java正则}",用字符串"(java正则)"替代
  • 其余部分全部转为字符常量(通过Pattern.quote实现),字符常量将精确匹配不作为任何模式匹配

举例:\\Q、\\E之间的就是字符常量,精确匹配"."就是真实的字符"."

替换前替换后路径示例备注

t?st.jsp

\\Qt\\E.\\Qst.jsp\\E

test.jsp\\Q、\\E之间的就是字符常量,精确匹配"."就是真实的字符"."
t*st.jsp\\Qt\\E.*\\Qst.jsp\\Etabcst.jsp
url_{view}url_(.*)url_abc123其中view作为路径变量名,abc123作为view的值
url_{view:[a-z]+}url_([a-z]+)url_abc其中view作为变量名,abc作为view的值,注意url_abc123无法命中,因为只能命中a-z中的字母

RequestMappingHandlerMapping匹配后处理(handleMatch)

匹配后处理主要处理三件事情

  1. 如果有PatternsRequestCondition匹配,则抽取路径变量并添加到requst属性"org.springframework.web.servlet.HandlerMapping.uriTemplateVariables"上
  2. 如果不支持"移除分号(;)"特性,即org.springframework.web.util.UrlPathHelper#removeSemicolonContent值为false,则解析【1】中的路径变量上的";"得到matrixVariables,并添加到request属性"org.springframework.web.servlet.HandlerMapping.matrixVariables"。将配合注解@MatrixVariable做参数解析
  3. 如果有ProducesRequestCondition匹配,则将对应的MediaType集合添加到request属性"org.springframework.web.servlet.HandlerMapping.producibleMediaTypes"上

源码现场在下面

public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {

......

    protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
		super.handleMatch(info, lookupPath, request);

		String bestPattern;
		Map<String, String> uriVariables;

		Set<String> patterns = info.getPatternsCondition().getPatterns();
		if (patterns.isEmpty()) {
			bestPattern = lookupPath;
			uriVariables = Collections.emptyMap();
		}
		else {
			bestPattern = patterns.iterator().next();
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
		}

		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);

		if (isMatrixVariableContentAvailable()) {
			Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
			request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
		}

		Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);

		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		}
	}

... ...
}

     如何抽取路径变量? 重新走了一遍AntPathMatcher的match过程,参考PatternsRequestCondition的匹配逻辑。

RequestMappingHandlerMapping配置

RequestMappingHandlerMapping配置方式很多,其实重要包装Spring IOC容器中有这个对象即可,任何配置Spring Bean的方式都可配置RequestMappingHandlerMapping。下面列以下典型的配置方式:

默认策略

    如果开发者什么也不配置,DispatcherServlet也会根据默认策略,创建一组默认的HandlerMappings其中就包括一个RequestMappingHandlerMapping。本文的开头就已经介绍了默认策略,此处不做赘述。

web.xml方式启动MVC

web.xml方式又有两种配置RequestMappingHandlerMapping的方法(1) 配置一个普通的bean  (2)利用Spring Schema机制配置标签<mvc:annotation-driven />

普通的Bean

最简单的配置方式,只要在Beans文件中配置一个简单RequestMappingHandlerMapping即可

Spring Schema机制

只需要配置<mvc:annotation-driven /> ,这种方式的原理在于Spring Schema机制《Spring中自定义Schema配置Bean》。spring-mvc的包中也实现了自定义的Schema-org.springframework.web.servlet.config.MvcNamespaceHandler。在MvcNamespaceHandler识别xml的标签<annotation-driven>并注册RequestMappingHandlerMapping类型的Bean到Spring IOC容器。

 

@EnableWebMvc方式启动MVC

@EnableWebMvc方式只需要在某个bean前面加入注解@EnableWebMvc即可

这种方式是因为@EnableWebMvc引入了org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration。DelegatingWebMvcConfiguration是WebMvcConfigurationSupport子类,WebMvcConfigurationSupport配置了@Bean注入RequestMappingHandlerMapping。 

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
... ...

    @Bean
	@SuppressWarnings("deprecation")
	public RequestMappingHandlerMapping requestMappingHandlerMapping(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider)     {

		RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
		mapping.setOrder(0);
		mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
		mapping.setContentNegotiationManager(contentNegotiationManager);
		mapping.setCorsConfigurations(getCorsConfigurations());

        ... ...
    }    

    ... ...
}

SpringBoot方式启动MVC

同样进入boot时代,配置更简单了,只要在pom中引入starter-web即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${boot.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 



上一篇  DispatcherServlet

下一篇 RequestMappingHandlerAdapter

再下一篇 参数解析器HandlerMethodArgumentResolver

 

 

 

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值