zoukankan      html  css  js  c++  java
  • SpringMVC之RequestMappingInfo详解之合并&请求匹配&排序

    写在前面

    1.RequestMapping 概述

    先来看一张图:

    从这张图,我们可以发现几个规律:

    1. @RequestMapping 的注解属性中,除了 name 不是数组,其他注解属性都支持数组。

    2. @RequestMapping 的注解属性中,除了 method 属性类型是枚举类型 RequestMethod,其他注解属性都用的 String 类型。

    3. DefaultBuilderRequestMappingInfo 的私有静态内部类,该类设计上使用了建造者模式。

    4. DefaultBuilder # build() 方法中,除 mappingName 以外的属性都被用来创建 RequestCondition 的子类实例。

    5. DefaultBuilder # build() 返回值是 RequestMappingInfo,该对象的构造函数包含 name 以及多个RequestCondition 的子类。

    本节接下来对各个参数逐组进行说明,熟悉的同学可以跳过。

    其中,@RequestMapping 的注解属性 RequestMethod[ ] method() 的数组元素会通过构造函数传递给 RequestMethodsRequestCondition ,用于构成成员变量 Set<RequestMethod> methods ,也比较简单,不多赘述。

    params 和 headers

    params 和 headers 相同点均有以下三种表达式格式:

    1. !param1: 表示允许不含有 param1 请求参数/请求头参数(以下简称参数)

    2. param2!=value2:表示允许不包含 param2 或者 虽然包含 param2 参数但是值不等于 value2;不允许包含param2参数且值等于value2

    3. param3=value3:表示需要包含 param3 参数且值等于 value3

    这三种表达式的解析逻辑来自 AbstractNameValueExpression 点击展开
    
    AbstractNameValueExpression(String expression) {
    	int separator = expression.indexOf('=');
    	if (separator == -1) {
    		this.isNegated = expression.startsWith("!");
    		this.name = (this.isNegated ? expression.substring(1) : expression);
    		this.value = null;
    	}
    	else {
    		this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!');
    		this.name = (this.isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator));
    		this.value = parseValue(expression.substring(separator + 1));
    	}
    }
    

    TIPSHeadersRequestCondition / ParamsRequestConditionHttpServletRequest 是否能够匹配,取决于 getMatchingCondition 方法。该方法返回 null 表示不匹配,有返回值表示可以匹配。

    params 和 headers 不同点大小写敏感度不同:

    params: 大小写敏感:"!Param" 和 "!param" ,前者表示不允许 Param 参数,后者则表示不允许 param 参数。反映在源码上,即 ParamsRequestCondition 的成员变量 expressions 包含 2 个 ParamExpression 对象。

    headers: 大小写不敏感:"X-Forwarded-For=unknown" 和 "x-forwarded-for=unknown" 表达式含义是一样的。反映在源码上,即 HeadersRequestCondition 的成员变量 expressions 仅包含 1 个 HeaderExpression 对象。

    headers 的额外注意点:

    headers={"Accept=application/*","Content-Type=application/*"}
    AcceptContent-Type 解析得到的 HeaderExpression 不会被添加到 HeadersRequestCondition 中。
    
    private static Collection parseExpressions(String... headers) {
    	Set expressions = new LinkedHashSet<>();
    	for (String header : headers) {
    		HeaderExpression expr = new HeaderExpression(header);
    		if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) {
    			continue;
    		}
    		expressions.add(expr);
    	}
    	return expressions;
    }
    

    consumes 和 produces

    consumes 和 produces 不同点

    headersAccept=valuevalue 会被 ProducesRequestCondition 解析。

    相对地,headersContent-Type=valuevalue 会被 ConsumesRequestCondition 解析。

    ProducesRequestCondition # parseExpressions
    
    

    private Set parseExpressions(String[] produces, @Nullable String[] headers) {
    Set result = new LinkedHashSet<>();
    if (headers != null) {
    for (String header : headers) {
    HeaderExpression expr = new HeaderExpression(header);
    if ("Accept".equalsIgnoreCase(expr.name) && expr.value != null) {
    for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
    result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated));
    }
    }
    }
    }
    for (String produce : produces) {
    result.add(new ProduceMediaTypeExpression(produce));
    }
    return result;
    }


    ConsumesRequestCondition # parseExpressions
    
    private static Set parseExpressions(String[] consumes, @Nullable String[] headers) {
    	Set result = new LinkedHashSet<>();
    	if (headers != null) {
    		for (String header : headers) {
    			HeaderExpression expr = new HeaderExpression(header);
    			if ("Content-Type".equalsIgnoreCase(expr.name) && expr.value != null) {
    				for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) {
    					result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated));
    				}
    			}
    		}
    	}
    	for (String consume : consumes) {
    		result.add(new ConsumeMediaTypeExpression(consume));
    	}
    	return result;
    }
    

    consumes 和 produces 相同点:均有正反 2 种表达式

    • 肯定表达式:"text/plain"

    • 否定表达式:"!text/plain"

    常见的类型,可以从 org.springframework.http.MediaType 引用,比如 MediaType.APPLICATION_JSON_VALUE = "application/json"


    MimeTypeUtils.parseMimeType 这个静态方法可以将字符串转换为 MimeType:
    
    public static MimeType parseMimeType(String mimeType) {
    	// 验证成分是否齐全
    	if (!StringUtils.hasLength(mimeType)) {
    		throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
    	}
    	// 如果包含分号(;),那么就取分号之前的部分进行解析
    	int index = mimeType.indexOf(';');
    	String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim();
    	if (fullType.isEmpty()) {
    		throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
    	}
    	// 遇上单个星号(*)转换成全通配符(*/*)
    	if (MimeType.WILDCARD_TYPE.equals(fullType)) {
    		fullType = "*/*";
    	}
    	// 斜杠左右两边分别是 type 和 subType
    	int subIndex = fullType.indexOf('/');
    	if (subIndex == -1) {
    		throw new InvalidMimeTypeException(mimeType, "does not contain '/'");
    	}
    	// subType 为空,抛出异常
    	if (subIndex == fullType.length() - 1) {
    		throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'");
    	}
    	String type = fullType.substring(0, subIndex);
    	String subtype = fullType.substring(subIndex + 1, fullType.length());
    	// type 为 *, subType 不为 * 是不合法的通配符格式, 例如  */json 
    	if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) {
    		throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)");
    	}
    	// 解析参数部分
    	Map parameters = null;
    	do {
    		int nextIndex = index + 1;
    		boolean quoted = false;
    		while (nextIndex < mimeType.length()) {
    			char ch = mimeType.charAt(nextIndex);
    			if (ch == ';') {
    				// 双引号之间的分号不能作为参数分割符,比如 name="Sam;Uncle" ,扫描到分号时,不会退出循环
    				if (!quoted) {
    					break;
    				}
    			}
    			else if (ch == '"') {
    				quoted = !quoted;
    			}
    			nextIndex++;
    		}
    		String parameter = mimeType.substring(index + 1, nextIndex).trim();
    		if (parameter.length() > 0) {
    			if (parameters == null) {
    				parameters = new LinkedHashMap<>(4);
    			}
    			// 等号分隔参数key和value
    			int eqIndex = parameter.indexOf('=');
    			// 如果没有等号,这个参数不会被解析出来,比如 ;hello; ,其中 hello 就不会被解析为参数 
    			if (eqIndex >= 0) {
    				String attribute = parameter.substring(0, eqIndex).trim();
    				String value = parameter.substring(eqIndex + 1, parameter.length()).trim();
    				parameters.put(attribute, value);
    			}
    		}
    		index = nextIndex;
    	}
    	while (index < mimeType.length());
    	try {
    		// 创建并返回一个 MimeType 对象
    		return new MimeType(type, subtype, parameters);
    	}
    	catch (UnsupportedCharsetException ex) {
    		throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetNam
    	}
    	catch (IllegalArgumentException ex) {
    		throw new InvalidMimeTypeException(mimeType, ex.getMessage());
    	}
    }
    

    MimeType 由三部分组成:类 type,子类 subType ,参数 parameters

    字符串结构为 type/subType;parameter1=value1;parameter2=value2;,常见的规则:

    • typesubType 不可以为空。

    • 分号 ; 可以用来分开 mineType 和 参数,还可以分隔多个参数。

    • 双引号""之间的分号 ; 将不会被识别为分隔符。

    • 如果 type 已经使用了 *subType 就只能是 **/json 这种表达式写法是不合法的,无法被解析。

    path 和 value

    1.如果一个注解中有一个名称为 value 的属性,且你只想设置value属性(即其他属性都采用默认值或者你只有一个value属性),那么可以省略掉“value=”部分。

    If there is just one element named value, then the name can be omitted. Docs. here

    // 就像这样使用,十分熟悉的“味道”
    @RequestMapping("/user")
    public class UserController { ... }
    

    2.pathvalue 不能同时有值。

    import org.junit.Test;
    import org.springframework.core.annotation.AnnotatedElementUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    
    public class AnnotationTest {
    
        @Test
        @RequestMapping(path = "hello", value = "hi")
        public void springAnnotation() throws NoSuchMethodException {
            Method springAnnotation = AnnotationTest.class.getDeclaredMethod("springAnnotation");
            RequestMapping annotation = AnnotatedElementUtils.findMergedAnnotation(springAnnotation, RequestMapping.class);
            System.out.println("value=" + Arrays.toString(annotation.value()));
            System.out.println("path=" + Arrays.toString(annotation.path()));
        }
    }
    

    上面这段代码会抛出一个 AnnotationConfigurationException 异常: pathvalue 只允许用其中一个。

    org.springframework.core.annotation.AnnotationConfigurationException: In annotation [org.springframework.web.bind.annotation.RequestMapping] declared on public void coderead.springframework.requestmapping.AnnotationTest.springAnnotation() throws java.lang.NoSuchMethodException 
    and synthesized from [@org.springframework.web.bind.annotation.RequestMapping(path=[hello], headers=[], method=[], name=, produces=[], params=[], value=[], consumes=[])]
    , attribute 'value' and its alias 'path' are present with values of [{hi}] and [{hello}], but only one is permitted.

    RequestMappingHandlerMapping # createRequestMappingInfo 方法,获取注解就是用的 AnnotatedElementUtils.findMergedAnnotation 方法。

    合并 combine

    • 合并字符串:
      1. 如果类和方法的 @RequestMapping 注解只有其中一个声明了 name 属性,那么选取不为 null 的这条即可。
      2. name:如果类和方法上都声明了 name 属性,那么需要用 # 连接字符串

    • 取并集
      + 选取类和方法的 @RequestMapping 注解属性 method[] / headers[] / params[] 的合集
      + RequestMethodsRequestCondition
      + HeadersRequestCondition
      + ParamsRequestCondition

    • 合并字符串且取并集
      + PatternsRequestCondition:如果类和方法上都声明了 value[] / path[] 属性,连接类和方法上的字符串组成新的表达式。
      + 具体的合并规则参考 AntPathMatcher#combine
      + 并集数量的类注解 value 数组长度 * 方法注解 value 数组长度 - 重复合并结果数量

    类路径 方法路径 合并路径
    /* /hotel /hotel
    /. /*.html /*.html
    /hotels/* /booking /hotels/booking
    /hotels/** /booking /hotels/**/booking
    /{foo} /bar /{foo}/bar
    • 细粒度覆盖粗粒度
      如果类和方法上的 @RequestMapping 注解的 consumes[] 和 produces[] 都不为 null,则方法上的注解属性覆盖类上的注解属性
      + ConsumesRequestCondition
      + ProducesRequestCondition

    请求匹配 getMatchingCondition

    1. 首先是 RequestMethodsRequestCondition ,这个比较是最简单的,只有有一个 method 和请求的 http 报文的 method 相同就就算匹配了
    2. 接着是比较简单的一类表达式 ParamsRequestCondition,HeadersRequestCondition,ConsumesRequestCondition,ProducesRequestCondition
    3. 最后才是 PatternsRequestCondition

    只有一个条件不匹配,就直接返回 null,否则继续执行到所有条件都匹配完成。

    排序 compareTo

    当存在多个 Match 对象时,自然要排出个次序来,因此,需要用到 compareTo 方法

    优先级:

    • 不包含 ** 优先于 包含 **
    • 一个 {param} 或者一个 * 记数一次,计数值小的优先
    • @PathVariable 字符串短优先匹配: {age} > {name}
    • /* 通配符用得少的优先
    • @PathVariable 用得少的优先

    假如在一个 Controller 中同时包含 /prefix/info , /prefix/{name} , /prefix/* , /prefix/** 这四个模式:

    请求url 匹配 patterns
    /prefix/info /prefix/info
    /prefix/hello /prefix/{name}
    /prefix /prefix/*
    /prefix/ /prefix/**
    /prefix/abc/123 /prefix/**

    总结

    在 Web 应用启动时,@RequestMapping 注解解析成 RequestMappingInfo 对象,并且注解的每个属性都解析成一个对应的 RequestCondition。

    通过对条件的筛选,选出符合条件的 RequestMappingInfo,如果包含多个 RequestMappingInfo,需要对条件进行排序,再选出优先级最高的一个 RequestMappingInfo。

    最后再通过 RequestMappingInfoHandlerMapping 获取对应的 HandlerMethod ,然后就可以封装执行过程了。

  • 相关阅读:
    技术的极限(8): 集成与分离
    心智与认知(1): 反馈循环(Feedback loop)
    证明与计算(6): 身份认证与授权
    证明与计算(5): 从加密哈希函数到一致性哈希
    技术的极限(6): 密码朋克精神(Cypherpunk Spirit)
    翻译(3): NULL-计算机科学上最糟糕的失误
    工具(5): 极简开发文档编写(How-to)
    证明与计算(3): 二分决策图(Binary Decision Diagram, BDD)
    证明与计算(2): 离散对数问题(Discrete logarithm Problem, DLP)
    翻译(2): How to Write a 21st Century Proof
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/springMvc-RequestMappingInfo.html
Copyright © 2011-2022 走看看