SpringMVC之映射处理阶段

在springMVC阶段中,对各个流程已经很熟悉了,下面再来深究一下细节。

本文主要讲解请求映射的建立和处理拦截器

根据以前自定义SpringMVC的基础条件中,可以知道我们的创建条件无非以下几个步骤:

  • 1、先扫描上下文;
  • 2、判断类和方法上是否存在着对应的注解,并将信息进行封装;
  • 3、添加映射;
  • 4、请求过来时,根据条件来处理请求;

所以猜想以下springmvc中应该也是这样子来进行实现的,无非是功能实现上丰富一点。

1、引言

DispatcherServlet通过SPI机制来加载默认提供的相关组件,而SPI的核心就在于DispathcerServlet.properties文件:

该文件内部列举了各个组件会提供哪些默认实现,使用这些默认实现的前提是,DispathcerServlet在初始化各个组件时,并没有在当前容器内发现各个组件已有的实现。[参考对应的配置类]

Controller的寻找是通过HandlerMapping完成的,而调用则是通过HandlerAdaptor完成的。

对于注解版本Controller寻找是通过RequestMappingHandlerMapping完成的,RequestMappingHandlerMapping主要负责在自身初始化阶段搜寻出当前容器内所有可用Controller实现,然后建立相关映射关系; 在请求到来时,再通过这些映射关系寻找到对应处理方法后返回。

对于注解版本的Controller请求处理方法调用是通过RequestMappingHandlerAdapter完成的,RequestMappingHandlerAdapter负责拿到RequestMappingHandlerMapping返回的方法后,进行一系列处理后,调用目标方法处理请求,这一系列处理包括: 数据绑定和数据校验,返回值处理等等…

整个注解版本Controller源码解析流程较为繁琐,但是大体上还是分为两个阶段:

  1. 映射建立

  2. 处理请求

因此,本节先分析前半部分,即RequestMappingHandlerMapping是如何建立映射关系的

2、RequestMappingHandlerMapping

类图分析:

  • AbstractHandlerMapping:提供基础设施支持,例如: 路径解析,拦截器,跨域。 规定了根据request得到handler的模板方法处理流程getHandler,具体如何根据request寻找到某个handler,则是由子类实现。

  • AbstractHandlerMethodMapping:囊括了对注解Controller寻找,建立映射和根据request找到对应handler的流程支持,核心在于建立Reuqest和HandlerMethod的映射关系,将识别处理器方法和建立映射的任务交给子类实现。

  • RequestMappingHandlerMapping:核心在于解析处理器方法和对应Controller上@RequestMapping注解,然后合并生成一个RequestMappingInfo作为映射的关键一环返回。

建立映射

首先肯定是需要有一个扫描环节的。那么直接看一下springMVC中提供的扫描环节。

Reuqest和HandlerMethod的映射的建立过程由AbstractHandlerMethodMapping实现的初始化回调接口afterPropertiesSet完成:

而实际上,另外一个接口也很重要:ApplicationContextAware中的setApplicationContext方法

	// Handler method detection

	/**
	 * Detects handler methods at initialization.
	 * @see #initHandlerMethods
	 */
	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}

看方法注释:查找得到Handlermethod

initHandlerMethods是映射建立的入口,我们需要深入其中:

protected void initHandlerMethods() {
    //getCandidateBeanNames可以简单的理解为是获取当前容器内部的所有bean实例
    for (String beanName : getCandidateBeanNames()) {
      		  ...
            //需要判断当前bean是不是我们需要的候选bean,如果是就进行处理
            processCandidateBean(beanName);
    }
    //简单的日志记录
    handlerMethodsInitialized(getHandlerMethods());
}

这里的前提是:controller已经成为bean了,然后才会去获取得到对应的bean的name。这个是一个大前提。

processCandidateBean是核心方法,该方法内部完成了bean的筛选和对某个Controller内部所有handlerMethod的探测。

	protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		// 获取当前bean类型
		beanType = obtainApplicationContext().getType(beanName);
	    ..
	    //默认AbstractHandlerMethodMapping是不提供对处理器的识别的,具体如何识别某个bean是不是handler,是由子类决定的
	    //这里是AbstractHandlerMethodMapping实现的,筛选规则如下:
	    //检验当前bean上是否存在Controller或者RequestMapping注解
		if (beanType != null && isHandler(beanType)) {
		    //如果当前bean是一个handler,那么需要探测出该handler内部所有handlerMethod实现
			detectHandlerMethods(beanName);
		}
	}

isHandler方法判断

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

从这里可以得到一个细节:Springmvc认为一个bean上如果存在着@Controller或者@RequestMapping注解,那么springmvc认为这个类是一个handler

但是因为编码习惯,通常认为MVC中的C表示的是controller层,只做参数处理和转发。我们不会在service层面和dao层面上来写Controller注解和RequestMapping注解

handlerMethod才是处理请求的终点,因此我们需要探测当前handler内部有哪些handlerMethod,并且建立好相关映射关系:

开始来查找对应的HandlerMethod,这里的查找是遍历每个类,查看类中的HandlerMethod

	protected void detectHandlerMethods(Object handler) {
	    //先获取到当前handler的type。handler分类比较多
        // 这里判断是否是以/beanName的
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());
                                    
		if (handlerType != null) {
            
		    //如果当前handler是被cglib代理过的对象,那么需要获取当前代理对象的superClass
		    //因为这才是目标handler的类型
			Class<?> userType = ClassUtils.getUserClass(handlerType);
            
			//MethodIntrospector类主要提供对方法的筛选和通用处理封装
			//这里selectMethods就是筛选出当前handler内部所有符合要求的handlerMethod
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
			        //筛选出某个handlerMethod,利用注册的回调接口生成映射关系
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			... 
			methods.forEach((method, mapping) -> {
			    //对jdk动态代理的情况进行处理--一般情况下可以忽略,因此controller层一般都是采用cglib代理
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				//注册
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}

MethodIntrospector.selectMethods作用可以简单看做是遍历handler类内所有方法,包括其父类和实现接口以及本类都做为一个特殊的handler。

为什么需要遍历父类和接口的类来作为特殊handler?因为spring中涉及到动态代理,而动态代理分为了类和接口两种方式。

这里是为了获取得到父类和父接口中的方法来进行操作。

看一下遍历方式:

// 将遍历结果放入到集合中来进行存放
final Map<Method, T> methodMap = new LinkedHashMap<>();


for (Class<?> currentHandlerType : handlerTypes) {
    final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);

    ReflectionUtils.doWithMethods(currentHandlerType, method -> {
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        // 重点是这里,对每个方法做了什么操作?得到的结果是什么?
        // 看外面的lambda表达式
        T result = metadataLookup.inspect(specificMethod);
        if (result != null) {
            // 过滤桥接方法,最终存入集合
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
            if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
                methodMap.put(specificMethod, result);
            }
        }
    }, ReflectionUtils.USER_DECLARED_METHODS);
}

那么看一下遍历过程中做了什么事情?大概意思是说过滤掉桥接方法,然后对每个方法来做操作

Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
	(MethodIntrospector.MetadataLookup<T>) method -> {
        try {
            // 对每个方法和对应你的类型生成映射
            return getMappingForMethod(method, userType);
        }
        catch (Throwable ex) {
            throw new IllegalStateException("Invalid mapping on handler class [" +
                                            userType.getName() + "]: " + method, ex);
        }
});

类和方法形成映射

和上面识别handler一样,具体是如何完成对method解析的过程,也是由RequestMappingHandlerMapping子类实现的。

3、解析handlerMethod

	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        // 根据Method和Class分别创建RequestMappingInfo
        // 类和方法上都可能存在RequestMapping注解
        
	   //根据当前方法,创建一个请求映射信息
		RequestMappingInfo info = createRequestMappingInfo(method);
		//如果当前方法并没有标注@RequestMapping等注解,那么也就不是一个handlerMethod,那么就返回null
		//该方法就会在selectMethods中被过滤掉
		if (info != null) {
		    //当前handlerMethod属于的handler上是否也存在@RequestMapping注解,如果存在就解析
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
			//如果handler上确实存在,那么就需要将方法上的@RequestMapping注解和类上的@RequestMapping注解注解进行合并
				info = typeInfo.combine(info);
			}
			//关于前缀的问题,下一节会展开讲,这里先跳过
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
			}
		}
		return info;
	}

createRequestMappingInfo方法会对传入的AnnotatedElement上的RequestMapping注解进行解析,然后生成RequestMappingInfo返回。

AnnotatedElement是JDK反射包提供的顶层接口,实现了该接口的元素,都是可以标注注解的元素,例如: Class,Method,Parameter等都实现了该接口
// 传递进来的是Method和Class类型
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
//拿到当前元素上的注解信息
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    
// 自定义实现过程    
//用户可以实现相关方法来创建自定义的请求匹配条件
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
//如果存在注解,就创建对应的RequestMappingInfo    
return (requestMapping != null ?createRequestMappingInfo(requestMapping, condition) : null);
}

注意

重点:自定义实现请求路径匹配条件

后续可以针对于各种各样的请求匹配规则来进行操作。

RequestMappingInfo可以看出,就是@RequestMapping注解对应信息的实体载体。

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

		RequestMappingInfo.Builder builder = RequestMappingInfo
		        //requestMapping注解中的path属性会经过EL解析器解析,也就是我们在路径中可以通过el表达式获取上下文中的值
		        //例如: ${user.dir}
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				//通过请求方式限制匹配
				.methods(requestMapping.method())
				//通过请求参数中必须携带某个请求参数进行限制匹配
				.params(requestMapping.params())
				//通过请求头中必须携带某个请求头进行限制匹配
				.headers(requestMapping.headers())
				//通过限制请求头中的content-type来进行限制匹配
				.consumes(requestMapping.consumes())
				//规定响应的content-type类型
				.produces(requestMapping.produces())
          		// 映射名称
				.mappingName(requestMapping.name());
	     //是否存在用户自定义匹配限制	
		if (customCondition != null) {
			builder.customCondition(customCondition);
		}
		//构建RequestMappingInfo后返回
		return builder.options(this.config).build();
	}

综合整个过程,无非就是分别按到类上的RequestMapping和方法上的RequestMapping注解信息,分别创建出来RequestMappingInfo对象,然后来进行合并(combine方法),类的在前,方法的在后。

无非就是组合起来形成最终的RequestMappingInfo

所以最终,在集合中存在是:

final Map<Method, T> methodMap = new LinkedHashMap<>();

Method和对应的RequestMappingInfo形成了映射关系。哪个方法对应的RequestMappingInfo是什么。

在RequestMappingInfo信息中,存储了当前controller中的所有的映射信息,只等用户请求过来了。

4、@RequestMapping注解

@RequestMapping注解中各个属性含义详解如下:

// @since 2.5 用于将Web请求映射到具有灵活方法签名的请求处理类中的方法的注释  Both Spring MVC and `Spring WebFlux` support this annotation
// @Mapping这个注解是@since 3.0  但它目前还只有这个地方使用到了~~~ 我感觉是多余的
@Target({ElementType.METHOD, ElementType.TYPE}) // 能够用到类上和方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

	//给这个Mapping取一个名字。若不填写,就用HandlerMethodMappingNamingStrategy去按规则生成
	String name() default "";

	// 路径  数组形式  可以写多个。  一般都是按照Ant风格进行书写~
	@AliasFor("path")
	String[] value() default {};
	@AliasFor("value")
	String[] path() default {};
	
	// 请求方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
	// 显然可以指定多个方法。如果不指定,表示适配所有方法类型~~
	// 同时还有类似的枚举类:org.springframework.http.HttpMethod
	RequestMethod[] method() default {};
	
	// 指定request中必须包含某些参数值时,才让该方法处理
	// 使用 params 元素,你可以让多个处理方法处理到同一个URL 的请求, 而这些请求的参数是不一样的
	// 如:@RequestMapping(value = "/fetch", params = {"personId=10"} 和 @RequestMapping(value = "/fetch", params =   		    	// {"personId=20"}
	// 这两个方法都处理请求`/fetch`,但是参数不一样,进入的方法也不一样~~~~
	// 支持!myParam和myParam!=myValue这种~~~
	String[] params() default {};

	// 指定request中必须包含某些指定的header值,才能让该方法处理请求
	// @RequestMapping(value = "/head", headers = {"content-type=text/plain"}
	String[] headers() default {};

	// 指定处理请求request的**提交内容类型**(Content-Type),例如application/json、text/html等
	// 相当于只有指定的这些Content-Type的才处理 
	// @RequestMapping(value = "/cons", consumes = {"application/json", "application/XML"}
	// 不指定表示处理所有~~  取值参见枚举类:org.springframework.http.MediaType
	// 它可以使用!text/plain形如这样非的表达方式
	String[] consumes() default {};
	// 指定返回的内容类型,返回的内容类型必须是request请求头(Accept)中所包含的类型
	// 仅当request请求头中的(Accept)类型中包含该指定类型才返回;
	// 参见枚举类:org.springframework.http.MediaType
	// 它可以使用!text/plain形如这样非的表达方式
	String[] produces() default {};

}

Spring4.3之后提供了组合注解5枚:

@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping

consumes 与 headers 区别

  • consumes 、produces 、params、 headers四个属性都是用来缩小请求范围。
  • consumes只能指定 content-Type 的内容类型,但是headers可以指定所有。

所以可以认为:headers是更为强大的(所有需要指定key和value嘛),而consumes和produces是专用的,头的key是固定的,所以只需要写value值即可,使用起来也更加的方便~。

推荐两个类

  • org.springframework.http.HttpHeaders,它里面有常量:几乎所有的请求头的key,以及我们可以很方便的构建一个HttpHeader,平时可以作为参考使用
  • org.springframework.http.MediaType,几乎支持所有的媒体类型,无论是请求的还是响应的媒体类型

合并定义

	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
	   //根据当前方法,创建一个请求映射信息
		RequestMappingInfo info = createRequestMappingInfo(method);
		//如果当前方法并没有标注@RequestMapping等注解,那么也就不是一个handlerMethod,那么就返回null
		//该方法就会在selectMethods中被过滤掉
		if (info != null) {
		    //当前handlerMethod属于的handler上是否也存在@RequestMapping注解,如果存在就解析
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
			//如果handler上确实存在,那么就需要将方法上的@RequestMapping注解和类上的@RequestMapping注解注解进行合并
				info = typeInfo.combine(info);
			}
			...
		}
		return info;
	}

如果当前handleMethod对应的Handler上也存在@RequestMapping注解,那么就需要将类上的提供的@RequestMapping注解信息,与当前类内部所有handlerMethod提供的@RequestMapping注解信息进行合并,具体合并规则如下:

请求路径就是拼接:

@RequestMapping("/admin")
@RestController
public class AdminController {
    @PostMapping("/login")
    public Result login(){
        ...
    }
}

HandlerMethod这里对应的就是login方法,而Handler对应的就是AdminController,此时合并完之后,得到的RequestMappingInfo 中的path路径为/admin/login。

其他属性就是简单的合并

5、注册HandlerMethod

	protected void detectHandlerMethods(Object handler) {
	    //先获取到当前handler的type
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());
                                    
		if (handlerType != null) {
		    //如果当前handler是被cglib代理过的对象,那么需要获取当前代理对象的superClass
		    //因为这才是目标handler的类型
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			//MethodIntrospector类主要提供对方法的筛选和通用处理封装
			//这里selectMethods就是筛选出当前handler内部所有符合要求的handlerMethod
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
			        //筛选出某个handlerMethod,利用注册的回调接口生成映射关系
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			... 
			methods.forEach((method, mapping) -> {
			    //对jdk动态代理的情况进行处理--一般情况下可以忽略,因此controller层一般都是采用cglib代理
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				//注册
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}

MethodIntrospector.selectMethods通过getMappingForMethod回调接口筛选出相关方法,并且建立好Method和对应RequestMappingInfo 映射关系后,返回了一个map集合,下面就是需要将这些映射关系进行注册。

子类RequestMappingHandlerMapping重写了父类的registerHandlerMethod方法,主要提供了对ConsumesCondition扩展点的支持:

	@Override
	protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
		super.registerHandlerMethod(handler, method, mapping);
		updateConsumesCondition(mapping, method);
	}

这里,我们先将目光着眼于父类AbstractHandlerMethodMapping提供的registerHandlerMethod实现:

	protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		this.mappingRegistry.register(mapping, handler, method);
	}

MappingRegistry映射注册中心

MappingRegistry是AbstractHandlerMethodMapping内部维护的一个映射关系的注册中心:

	class MappingRegistry {
        //保存RequestMappingInfo和MappingRegistration的映射关系
		private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
        //保存请求路径和RequestMappingInfo的映射关系
        // 注意这里的T可能是一个list
		private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
        //保存handlerMethodName和handlerMethod的映射关系  
		private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
        //保存handlerMethod和跨域配置的映射关系
		private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
        //读写锁 
		private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
		
		...

看下具体的过程,考虑到并发修改注册中心带来的安全性,这里采用了写锁:

   //mapping就是handlerMethod对应的RequestMappingInfo
   //然后是handler和handlerMethod
   //ps:这里的handlerMethod指的是handler中的处理请求方法
	public void register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
			    //对原生处理请求方法进行了一层封装,包装为了一个HandlerMethod 
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				//进行映射校验,判断是否存在模糊映射,即一个请求URL可以同时被多个handlerMethod处理
				validateMethodMapping(handlerMethod, mapping);
                //从RequestMappingInfo中获取当前handlerMethod能够处理的请求URL集合
				Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
				//将请求路径和RequestMappingInfo的映射关系添加到pathLookUp集合中保存
				for (String path : directPaths) {
					this.pathLookup.add(path, mapping);
				}
               
                  // 为HandlerMethod的映射分配名称
	              // 默认采用:RequestMappingInfoHandlerMethodMappingNamingStrategy 策略来分配名称
	             // 策略为:@RequestMapping指定了name属性,那就以指定的为准  否则策略为:取出Controller所有的`大写字母` + # + method.getName()
	            // 如:AppoloController#match方法  最终的name为:AC#match 
				String name = null;
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					addMappingName(name, handlerMethod);
				}
                
                //处理方法上的CrossOrigin跨域注解---这个后面讲到跨域问题的时候再说,本文不展开 
				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
				if (corsConfig != null) {
					corsConfig.validateAllowCredentials();
					this.corsLookup.put(handlerMethod, corsConfig);
				}
                //注册---这里是RequestMappingInfo和封装后的MappingRegistration的映射
				this.registry.put(mapping,
						new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
			}
			finally {
			    //释放写锁
				this.readWriteLock.writeLock().unlock();
			}
		}

createHandlerMethod

	protected HandlerMethod createHandlerMethod(Object handler, Method method) {
		if (handler instanceof String) {
			return new HandlerMethod((String) handler,
					obtainApplicationContext().getAutowireCapableBeanFactory(),
					obtainApplicationContext(),
					method);
		}
		return new HandlerMethod(handler, method);
	}

validateMethodMapping

		private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
			MappingRegistration<T> registration = this.registry.get(mapping);
			HandlerMethod existingHandlerMethod = (registration != null ? registration.getHandlerMethod() : null);
			if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
				throw new IllegalStateException(
						"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
						handlerMethod + "\nto " + mapping + ": There is already '" +
						existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
			}
		}

6、请求时处理映射

请求的时候一定会走DispatcherServlet中的getHandler方法

mappedHandler = getHandler(processedRequest);

然后根据请求路径找到对应的HandlerMethod

HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);

看看是如何找到的

// 从pathLookup中来进行查找集合,因为存在/user,但是请求方式不同的方法
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);

然后选择一个最佳匹配的HandlerMethod

然后会有两个设置:

request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);

关键是第二个:

protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
    super.handleMatch(info, lookupPath, request);
	
    RequestCondition<?> condition = info.getActivePatternsCondition();
    if (condition instanceof PathPatternsRequestCondition) {
        extractMatchDetails((PathPatternsRequestCondition) condition, lookupPath, request);
    }
    else {
        extractMatchDetails((PatternsRequestCondition) condition, lookupPath, request);
    }
	// 如果设置了产生的媒体类型,那么这里就直接存入request作用域中来。然后在内容协商的时候获取得到
    if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
        Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
        request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
    }
}

直接到内容协商org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getProducibleMediaTypes(javax.servlet.http.HttpServletRequest, java.lang.Class<?>, java.lang.reflect.Type)

protected List<MediaType> getProducibleMediaTypes(
    HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
	// 这里如果设置了,那么就不会走下面的匹配逻辑了
    // 而是直接返回,加快响应效率
    Set<MediaType> mediaTypes =
        (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<>(mediaTypes);
    }
    // 不光不走这里了,还会减少外层的匹配
    else if (!this.allSupportedMediaTypes.isEmpty()) {
        List<MediaType> result = new ArrayList<>();
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            if (converter instanceof GenericHttpMessageConverter && targetType != null) {
                if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            else if (converter.canWrite(valueClass, null)) {
                result.addAll(converter.getSupportedMediaTypes());
            }
        }
        return result;
    }
    else {
        return Collections.singletonList(MediaType.ALL);
    }
}

而且默认基于请求头的内容协商器也可以砂纸Accept字段中的值,设置期望得到什么媒体类型的数据。和能够产生的媒体类型来进行匹配。

posted @ 2022-09-22 14:48  雩娄的木子  阅读(192)  评论(0编辑  收藏  举报