User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   正文
Spring

SpringMVC—HandlerMethodArgumentResolver参数处理器

2022-10-10 12:17:24
1372  0 1
参考目录 隐藏
1) HandlerMethodArgumentResolver
2) 基于Name
3) PathVariableMethodArgumentResolver
4) 关于@PathVariable的required=false使用注意事项
5) RequestParamMethodArgumentResolver
6) get请求如何传值数组、集合(List)
7) @RequestParam传参
8) RequestHeaderMethodArgumentResolver
9) RequestAttributeMethodArgumentResolver
10) SessionAttributeMethodArgumentResolver
11) AbstractCookieValueMethodArgumentResolver(抽象类)
12) ServletCookieValueMethodArgumentResolver
13) MatrixVariableMethodArgumentResolver
14) ExpressionValueMethodArgumentResolver
15) 参数类型是Map的
16) PathVariableMapMethodArgumentResolver
17) RequestParamMapMethodArgumentResolver
18) RequestHeaderMapMethodArgumentResolver
19) MatrixVariableMapMethodArgumentResolver
20) MapMethodProcessor
21) 固定参数类型
22) ServletRequestMethodArgumentResolver
23) ServletResponseMethodArgumentResolver
24) SessionStatusMethodArgumentResolver
25) UriComponentsBuilderMethodArgumentResolver
26) RedirectAttributesMethodArgumentResolver
27) ModelMethodProcessor
28) 基于ContentType消息转换器类型
29) RequestPartMethodArgumentResolver
30) AbstractMessageConverterMethodProcessor(重点)
31) RequestResponseBodyMethodProcessor
32) HttpEntityMethodProcessor
33) ErrorsMethodArgumentResolver
34) Spring MVC参数处理器的注册与顺序
35) 自定参数解析器处理特定场景需求
36) 场景一:
37) 如何使用Spring容器内的Bean?
38) PropertyNamingStrategy
39) fastjson
40) jackson
41) 场景二:

阅读完需:约 61 分钟

在享受Spring MVC带给你便捷的时候,你是否曾经这样疑问过:Controller的handler方法参数能够自动完成参数封装(有时即使没有@PathVariable、@RequestParam、@RequestBody等注解都可),甚至在方法参数任意位置写HttpServletRequest、HttpSession、Writer…等类型的参数,它自动就有值了便可直接使用。

Spring MVC它只需要区区几个注解就能够让一个普通的java方法成为一个Handler处理器,并且还能有自动参数封装、返回值视图处理/渲染等一系列强大功能,让coder的精力更加的聚焦在自己的业务。

HandlerMethodArgumentResolver

策略接口:用于在给定请求的上下文中将方法参数解析为参数值。简单的理解为:它负责处理你Handler方法里的所有入参:包括自动封装、自动赋值、校验等等。有了它才能会让Spring MVC处理入参显得那么高级、那么自动化。

有个形象的公式:HandlerMethodArgumentResolver = HandlerMethod + Argument(参数) + Resolver(解析器)。

解释为:它是HandlerMethod方法的解析器,将HttpServletRequest(header + body 中的内容)解析为HandlerMethod方法的参数(method parameters)

// @since 3.1   HandlerMethod 方法中 参数解析器
public interface HandlerMethodArgumentResolver {

	// 判断 HandlerMethodArgumentResolver 是否支持 MethodParameter
	// (PS: 一般都是通过 参数上面的注解|参数的类型)
	boolean supportsParameter(MethodParameter parameter);
	
	// 从NativeWebRequest中获取数据,ModelAndViewContainer用来提供访问Model
	// MethodParameter parameter:请求参数
	// WebDataBinderFactory用于创建一个WebDataBinder用于数据绑定、校验
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

基于这个接口的处理器实现类不可谓不丰富,非常之多。我截图如下:

可以分为四类进行描述

  1. 基于Name
  2. 数据类型是Map的
  3. 固定参数类型
  4. 基于ContentType的消息转换器

基于Name

从URI(路径变量)、HttpServletRequest、HttpSession、Header、Cookie…等中根据名称key来获取值

这类处理器所有的都是基于抽象类AbstractNamedValueMethodArgumentResolver来实现,它是最为重要的分支(分类)。

// @since 3.1  负责从路径变量、请求、头等中拿到值。(都可以指定name、required、默认值等属性)
// 子类需要做如下事:获取方法参数的命名值信息、将名称解析为参数值
// 当需要参数值时处理缺少的参数值、可选地处理解析值

//特别注意的是:默认值可以使用${}占位符,或者SpEL语句#{}是木有问题的
public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Nullable
	private final ConfigurableBeanFactory configurableBeanFactory;
	@Nullable
	private final BeanExpressionContext expressionContext;
	private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);

	public AbstractNamedValueMethodArgumentResolver() {
		this.configurableBeanFactory = null;
		this.expressionContext = null;
	}
	public AbstractNamedValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
		this.configurableBeanFactory = beanFactory;
		// 默认是RequestScope
		this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);
	}

	// protected的内部类  所以所有子类(注解)都是用友这三个属性值的
	protected static class NamedValueInfo {
		private final String name;
		private final boolean required;
		@Nullable
		private final String defaultValue;
		public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
			this.name = name;
			this.required = required;
			this.defaultValue = defaultValue;
		}
	}

	// 核心方法  注意此方法是final的,并不希望子类覆盖掉他~
	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		// 创建 MethodParameter 对应的 NamedValueInfo
		NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
		// 支持到了Java 8 中支持的 java.util.Optional
		MethodParameter nestedParameter = parameter.nestedIfOptional();

		// name属性(也就是注解标注的value/name属性)这里既会解析占位符,还会解析SpEL表达式,非常强大
		// 因为此时的 name 可能还是被 ${} 符号包裹, 则通过 BeanExpressionResolver 来进行解析
		Object resolvedName = resolveStringValue(namedValueInfo.name);
		if (resolvedName == null) {
			throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
		}


		// 模版抽象方法:将给定的参数类型和值名称解析为参数值。  由子类去实现
		// @PathVariable     --> 通过对uri解析后得到的decodedUriVariables值(常用)
		// @RequestParam     --> 通过 HttpServletRequest.getParameterValues(name) 获取(常用)
		// @RequestAttribute --> 通过 HttpServletRequest.getAttribute(name) 获取   <-- 这里的 scope 是 request
		// @SessionAttribute --> 略
		// @RequestHeader    --> 通过 HttpServletRequest.getHeaderValues(name) 获取
		// @CookieValue      --> 通过 HttpServletRequest.getCookies() 获取
		Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);

		// 若解析出来值仍旧为null,那就走defaultValue (若指定了的话)
		if (arg == null) {
			// 可以发现:defaultValue也是支持占位符和SpEL的~~~
			if (namedValueInfo.defaultValue != null) {
				arg = resolveStringValue(namedValueInfo.defaultValue);

			// 若 arg == null && defaultValue == null && 非 optional 类型的参数 则通过 handleMissingValue 来进行处理, 一般是报异常
			} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
				
				// 它是个protected方法,默认抛出ServletRequestBindingException异常
				// 各子类都复写了此方法,转而抛出自己的异常(但都是ServletRequestBindingException的异常子类)
				handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
			}
	
			// handleNullValue是private方法,来处理null值
			// 针对Bool类型有这个判断:Boolean.TYPE.equals(paramType) 就return Boolean.FALSE;
			// 此处注意:Boolean.TYPE = Class.getPrimitiveClass("boolean") 它指的基本类型的boolean,而不是Boolean类型哦~~~
			// 如果到了这一步(value是null),但你还是基本类型,那就抛出异常了(只有boolean类型不会抛异常哦~)
			// 这里多嘴一句,即使请求传值为&bool=1,效果同bool=true的(1:true 0:false) 并且不区分大小写哦(TrUe效果同true)
			arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
		}
		// 兼容空串,若传入的是空串,依旧还是使用默认值(默认值支持占位符和SpEL)
		else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
			arg = resolveStringValue(namedValueInfo.defaultValue);
		}

		// 完成自动化的数据绑定~~~
		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
			try {
				// 通过数据绑定器里的Converter转换器把arg转换为指定类型的数值
				arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
			} catch (ConversionNotSupportedException ex) { // 注意这个异常:MethodArgumentConversionNotSupportedException  类型不匹配的异常
				throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
						namedValueInfo.name, parameter, ex.getCause());
			} catch (TypeMismatchException ex) { //MethodArgumentTypeMismatchException是TypeMismatchException 的子类
				throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
						namedValueInfo.name, parameter, ex.getCause());

			}
		}

		// protected的方法,本类为空实现,交给子类去复写(并不是必须的)
		// 唯独只有PathVariableMethodArgumentResolver把解析处理啊的值存储一下数据到 
		// HttpServletRequest.setAttribute中(若key已经存在也不会存储了)
		handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
		return arg;
	}


	// 此处有缓存,记录下每一个MethodParameter对象   value是NamedValueInfo值
	private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
		NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
		if (namedValueInfo == null) {
			// createNamedValueInfo是抽象方法,子类必须实现
			namedValueInfo = createNamedValueInfo(parameter);
			// updateNamedValueInfo:这一步就是我们之前说过的为何Spring MVC可以根据参数名封装的方法
			// 如果info.name.isEmpty()的话(注解里没指定名称),就通过`parameter.getParameterName()`去获取参数名~
			// 它还会处理注解指定的defaultValue:`\n\t\.....`等等都会被当作null处理
			// 都处理好后:new NamedValueInfo(name, info.required, defaultValue);(相当于吧注解解析成了此对象嘛~~)
			namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
			this.namedValueInfoCache.put(parameter, namedValueInfo);
		}
		return namedValueInfo;
	}

	// 抽象方法 
	protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
	// 由子类根据名称,去把值拿出来
	protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception;
}

该抽象类中定义了解析参数的主逻辑(模版逻辑),子类只需要实现对应的抽象模版方法即可。

对此部分的处理步骤,我把它简述如下:

  1. 基于MethodParameter构建NameValueInfo <– 主要有name, defaultValue, required(其实主要是解析方法参数上标注的注解~)
  2. 通过BeanExpressionResolver(${}占位符以及SpEL) 解析name
  3. 通过模版方法resolveName从 HttpServletRequest, Http Headers, URI template variables 等等中获取对应的属性值(具体由子类去实现)
  4. 对 arg==null这种情况的处理, 要么使用默认值, 若 required = true && arg == null, 则一般报出异常(boolean类型除外~)
  5. 通过WebDataBinder将arg转换成Methodparameter.getParameterType()类型(注意:这里仅仅只是用了数据转换而已,并没有用bind()方法)

该抽象类继承树如下:

从上源码可以看出,抽象类已经定死了处理模版(方法为final的),留给子类需要做的事就不多了,大体还有如下三件事:

  1. 根据MethodParameter创建NameValueInfo(子类的实现可继承自NameValueInfo,就是对应注解的属性们)
  2. 根据方法参数名称name从HttpServletRequest, Http Headers, URI template variables等等中获取属性值
  3. 对arg == null这种情况的处理(非必须)

PathVariableMethodArgumentResolver

它帮助Spring MVC实现restful风格的URL。它用于处理标注有@PathVariable注解的方法参数,用于从URL中获取值(并不是?后面的参数哦)。

并且它还可以解析@PathVariable注解的value值不为空的Map,使用较少

UriComponentsContributor接口:通过查看方法参数和参数值并决定应更新目标URL的哪个部分,为构建UriComponents的策略接口。

// @since 4.0 出现得还是比较晚的
public interface UriComponentsContributor {

	// 此方法完全同HandlerMethodArgumentResolver的这个方法~~~
	boolean supportsParameter(MethodParameter parameter);
	// 处理给定的方法参数,然后更新UriComponentsbuilder,或者使用uri变量添加到映射中,以便在处理完所有参数后用于扩展uri~~~
	void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder,
			Map<String, Object> uriVariables, ConversionService conversionService);
}

它的三个实现类:

// @since 3.0 需要注意的是:它只支持标注在@RequestMapping的方法(处理器)上使用~
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
	@AliasFor("name")
	String value() default "";
	@AliasFor("value")
	String name() default "";
	
	// 注意:它并没有defaultValue哦~

	// @since 4.3.3  它也是标记为false非必须的~~~~
	boolean required() default true;
}

// @since 3.1
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {
	private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);


	// 简单一句话描述:@PathVariable是必须,不管你啥类型
	// 标注了注解,且是Map类型,
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if (!parameter.hasParameterAnnotation(PathVariable.class)) {
			return false;
		}
		if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
			PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
			return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
		}
		return true;
	}

	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
		return new PathVariableNamedValueInfo(ann);
	}
	private static class PathVariableNamedValueInfo extends NamedValueInfo {
		public PathVariableNamedValueInfo(PathVariable annotation) {
			// 默认值使用的DEFAULT_NONE~~~
			super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
		}
	}

	// 根据name去拿值的过程非常之简单,但是它和前面的只知识是有关联的
	// 至于这个attr是什么时候放进去的,AbstractHandlerMethodMapping.handleMatch()匹配处理器方法上
	// 通过UrlPathHelper.decodePathVariables() 把参数提取出来了,然后放进request属性上暂存了~~~
	// 关于HandlerMapping内容,可来这里:https://blog.csdn.net/f641385712/article/details/89810020
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
		Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
		return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
	}

	// MissingPathVariableException是ServletRequestBindingException的子类
	@Override
	protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
		throw new MissingPathVariableException(name, parameter);
	}


	// 值完全处理结束后,把处理好的值放进请求域,方便view里渲染时候使用~
	// 抽象父类的handleResolvedValue方法,只有它复写了~
	@Override
	@SuppressWarnings("unchecked")
	protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {

		String key = View.PATH_VARIABLES;
		int scope = RequestAttributes.SCOPE_REQUEST;
		Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
		if (pathVars == null) {
			pathVars = new HashMap<>();
			request.setAttribute(key, pathVars, scope);
		}
		pathVars.put(name, arg);
	}
	...
}

说明:因为使用路径参数需要进行复杂的匹配流程以及正则匹配,所有效率相较来说低些,若以若是那种对响应事件强要求的(比如记录点击事件…),建议用请求参数代替(当然你也可以重写RequestMappingHandlerMapping的URL匹配方法来定制化你的需求)。

GET /list/cityId/1 属于RESTful /list/cityId?cityId=1不属于RESTful。通过Apache JMeter测试:非RESTful接口的性能是RESTful接口的两倍,接口相应时间上更是达到10倍左右(是–>300ms左右 非–>20ms左右)

针对RESTful:若你是一个现成的系统,现对相应提出要求:接口耗时必须控制在50ms以内,怎么办?

思路一:将所有的url修改为非RESTful风格(不使用@PathVariable)
痛点:系统已存在几百个接口,若修改不仅需要修改服务端,客户端也得改,工作量太大。并且稍有不慎,容易造成404现象~

思路二:定制化AbstractHandlerMethodMapping#lookupHandlerMethod方法
此方法负责URL的匹配,我们为了提效其实就是为了避免一些正则匹配(AntPathMatcher)。

唯一需要说一下如果类型是Map类型的情况下的使用注意事项,如下:

@PathVariable("jsonStr") Map<String,Object> map

希望把jsonStr对应的字符串解析成键值对封装进Map里。那么你必须,必须,必须注册了能处理此字符串的Converter/PropertyEditor(自定义)。

关于@PathVariable的required=false使用注意事项
@ResponseBody
@GetMapping("/test/{id}")
public Person test(@PathVariable(required = false) Integer id) { ... }

以为这样写通过/test这个url就能访问到了,其实这样是不行的,会404。

正确姿势:

@ResponseBody
@GetMapping({"/test/{id}", "/test"})
public Person test(@PathVariable(required = false) Integer id) { ... }

这样/test和/test/1这两个url就都能正常work了

@PathVariable的required=false使用较少,一般用于在用URL传多个值时,但有些值是非必传的时候使用。比如这样的URL:"/user/{id}/{name}","/user/{id}","/user"

RequestParamMethodArgumentResolver

顾名思义,是解析标注有@RequestParam的方法入参解析器,这个注解比上面的注解强大很多了,它用于从请求参数(?后面的)中获取值完成封装。这是我们的绝大多数使用场景。除此之外,它还支持MultipartFile,也就是说能够从MultipartHttpServletRequest | HttpServletRequest 获取数据,并且还兜底处理没有标注任何注解的“简单类型”

// @since 2.5
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
	@AliasFor("name")
	String value() default "";
	 // @since 4.2
	@AliasFor("value")
	String name() default "";
	boolean required() default true;
	String defaultValue() default ValueConstants.DEFAULT_NONE;
}
// @since 3.1
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor {

	private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);

	// 这个参数老重要了:
	// true:表示参数类型是基本类型 参考BeanUtils#isSimpleProperty(什么Enum、Number、Date、URL、包装类型、以上类型的数组类型等等)
	// 如果是基本类型,即使你不写@RequestParam注解,它也是会走进来处理的~~~(这个@PathVariable可不会哟~)
	// fasle:除上以外的。  要想它处理就必须标注注解才行哦,比如List等~
	// 默认值是false
	private final boolean useDefaultResolution;

	// 此构造只有`MvcUriComponentsBuilder`调用了  传入的false
	public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
		this.useDefaultResolution = useDefaultResolution;
	}
	// 传入了ConfigurableBeanFactory ,所以它支持处理占位符${...} 并且支持SpEL了
	// 此构造都在RequestMappingHandlerAdapter里调用,最后都会传入true来Catch-all Case  这种设计挺有意思的
	public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
		super(beanFactory);
		this.useDefaultResolution = useDefaultResolution;
	}

	// 此处理器能处理如下Case:
	// 1、所有标注有@RequestParam注解的类型(非Map)/ 注解指定了value值的Map类型(自己提供转换器哦)
	// ======下面都表示没有标注@RequestParam注解了的=======
	// 1、不能标注有@RequestPart注解,否则直接不处理了
	// 2、是上传的request:isMultipartArgument() = true(MultipartFile类型或者对应的集合/数组类型  或者javax.servlet.http.Part对应结合/数组类型)
	// 3、useDefaultResolution=true情况下,"基本类型"也会处理
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if (parameter.hasParameterAnnotation(RequestParam.class)) {
			if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
				RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
				return (requestParam != null && StringUtils.hasText(requestParam.name()));
			} else {
				return true;
			}
		} else {
			if (parameter.hasParameterAnnotation(RequestPart.class)) {
				return false;
			}
			parameter = parameter.nestedIfOptional();
			if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
				return true;
			} else if (this.useDefaultResolution) {
				return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
			} else {
				return false;
			}
		}
	}


	// 从这也可以看出:即使木有@RequestParam注解,也是可以创建出一个NamedValueInfo来的
	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
		return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
	}

	// 内部类
	private static class RequestParamNamedValueInfo extends NamedValueInfo {
		// 请注意这个默认值:如果你不写@RequestParam,那么就会用这个默认值
		// 注意:required = false的哟(若写了注解,required默认可是true,请务必注意区分)
		// 因为不写注解的情况下,若是简单类型参数都是交给此处理器处理的。所以这个机制需要明白
		// 复杂类型(非简单类型)默认是ModelAttributeMethodProcessor处理的
		public RequestParamNamedValueInfo() {
			super("", false, ValueConstants.DEFAULT_NONE);
		}
		public RequestParamNamedValueInfo(RequestParam annotation) {
			super(annotation.name(), annotation.required(), annotation.defaultValue());
		}
	}

	// 核心方法:根据Name 获取值(普通/文件上传)
	// 并且还有集合、数组等情况
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
		HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

		// 这块解析出来的是个MultipartFile或者其集合/数组
		if (servletRequest != null) {
			Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
			if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
				return mpArg;
			}
		}

		Object arg = null;
		MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
		if (multipartRequest != null) {
			List<MultipartFile> files = multipartRequest.getFiles(name);
			if (!files.isEmpty()) {
				arg = (files.size() == 1 ? files.get(0) : files);
			}
		}

		// 若解析出来值仍旧为null,那处理完文件上传里木有,那就去参数里取吧
		// 由此可见:文件上传的优先级是高于请求参数的
		if (arg == null) {
		
			//小知识点:getParameter()其实本质是getParameterNames()[0]的效果
			// 强调一遍:?ids=1,2,3 结果是["1,2,3"](兼容方式,不建议使用。注意:只能是逗号分隔)
			// ?ids=1&ids=2&ids=3  结果是[1,2,3](标准的传值方式,建议使用)
			// 但是Spring MVC这两种都能用List接收  请务必注意他们的区别~~~
			String[] paramValues = request.getParameterValues(name);
			if (paramValues != null) {
				arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
			}
		}
		return arg;
	}
	...
}

可以看到这个ArgumentResolver处理器还是很强大的:不仅能处理标注了@RequestParam的参数,还能接收文件上传参数。甚至那些你平时使用中不标注该注解的封装也是它来兜底完成的。至于它如何兜底的,可以参见下面这个骚操作:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
	...
	private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
		// Annotation-based argument resolution
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
		...
		// Catch-all  兜底
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
		resolvers.add(new ServletModelAttributeMethodProcessor(true));

		return resolvers;
	}
	...
}

可以看到ServletModelAttributeMethodProcessor和RequestParamMethodArgumentResolver一样,也是有兜底的效果的。

get请求如何传值数组、集合(List)

如题的这个case太常见了有没有,我们经常会遇到使用get请求向后端需要传值的需求(比如根据ids批量查询)。但到底如何传,URL怎么写,应该是有傻傻分不清楚的不确定的情况。

    @ResponseBody
    @GetMapping("/test/{objects}")
    public Object test(@PathVariable List<Object> objects) {
        System.out.println(objects);
        return objects;
    }

请求URL:/test/fsx,fsx,fsx。控制台打印:

[fsx, fsx, fsx]

集合接收成功(使用@PathVariable Object[] objects也是可以正常接收的)。

使用时应注意如下两点:

  • 多个值只能使用,号分隔才行(否则会被当作一个值,放进数组/集合里,不会报错)
  • @PathVariable注解是必须的。否则会交给ServletModelAttributeMethodProcessor兜底去处理,它要求有空构造所以反射创建实例会报错(数组/List)。(注意:如果是这样写ArrayList objects,那是不会报错的,只是值肯定是封装不进来的,一个空对象而已)

说明:为何逗号分隔的String类型默认就能转化为数组,集合。请参考StringToCollectionConverter/StringToArrayConverter这种内置的GenericConverter通用转换器

@RequestParam传参

    @ResponseBody
    @GetMapping("/test")
    public Object test(@RequestParam List<Object> objects) {
        System.out.println(objects);
        return objects;
    }

请求URL:/test/?objects=1,2,3。控制台打印:

[1, 2, 3]

请求URL改为:/test/?objects=1&objects=2&objects=3。控制台打印:

[1, 2, 3]

两个请求的URL不一样,但都能正确的达到效果。(@RequestParam Object[] objects这么写两种URL也能正常封装)

对此有如下这个细节你必须得注意:对于集合List而言@RequestParam注解是必须存在的,否则报错如下(因为交给兜底处理了):

但如果你这么写String[] objects,即使不写注解,也能够正常完成正确封装。

说明:Object[] objects这么写的话不写注解是不行的(报错如上)

需要注意的是,Spring MVC的这么多HandlerMethodArgumentResolver它的解析是有顺序的:如果多个HandlerMethodArgumentResolver都可以解析某一种类型,以顺序在前面的先解析(后面的就不会再执行解析了)。

源码参考处:HandlerMethodArgumentResolverComposite.getArgumentResolver(MethodParameter parameter);

由于RequestParamMethodArgumentResolver同样可以对Multipart文件上传进行解析,并且默认顺序在RequestPartMethodArgumentResolver之前,所以如果不添加@RequestPart注解,Multipart类型的参数会被RequestParamMethodArgumentResolver解析。

这里介绍了HandlerMethodArgumentResolver的功能和基本使用,以及深入介绍了最为重要的两个注解@PathVariable和@RequestParam以及各自对应的ArgumentResolver处理器。

RequestHeaderMethodArgumentResolver

@RequestHeader注解,可以把Request请求header部分的值绑定到方法的参数上。

public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {

	// 必须标注@RequestHeader注解,并且不能,不能,不能是Map类型
	// 有的小伙伴会说:`@RequestHeader Map headers`这样可以接收到所有的请求头啊
	// 其实不是本类的功劳,是`RequestHeaderMapMethodArgumentResolver`的作用
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(RequestHeader.class) &&
				!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()));
	}

	// 理解起来很简单:可以单值,也可以List/数组
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
		String[] headerValues = request.getHeaderValues(name);
		if (headerValues != null) {
			return (headerValues.length == 1 ? headerValues[0] : headerValues);
		} else {
			return null;
		}
	}
}

此处理器能处理的是我们这么来使用:

    @ResponseBody
    @GetMapping("/test")
    public Object test(@RequestHeader("Accept-Encoding") String encoding,
                       @RequestHeader("Accept-Encoding") List<String> encodingList) {
        System.out.println(encoding);
        System.out.println(encodingList);
        return encoding;
    }

请求头截图:

结果打印(集合封装成功了,证明逗号分隔是可以被封装成集合/数组的):

gzip, deflate, br
[gzip, deflate, br]

注解指定的value值(key值)是不区分大小写的

RequestAttributeMethodArgumentResolver

处理必须标注有@RequestAttribute注解的参数,原理说这一句话就够了。

return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST);

SessionAttributeMethodArgumentResolver

同上(注解不一样,scope不一样而已)

AbstractCookieValueMethodArgumentResolver(抽象类)

对解析标注有@CookieValue的做了一层抽象,子类负责从request里拿值(该抽象类不合请求域绑定)。

public abstract class AbstractCookieValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	...
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(CookieValue.class);
	}	
	@Override
	protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
		throw new MissingRequestCookieException(name, parameter);
	}
	... // 并木有实现核心resolveName方法
}

ServletCookieValueMethodArgumentResolver

指定了从HttpServletRequest去拿cookie值。

public class ServletCookieValueMethodArgumentResolver extends AbstractCookieValueMethodArgumentResolver {
	private UrlPathHelper urlPathHelper = new UrlPathHelper();
	...
	public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
		this.urlPathHelper = urlPathHelper;
	}

	@Override
	@Nullable
	protected Object resolveName(String cookieName, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
		HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
		Assert.state(servletRequest != null, "No HttpServletRequest");

		// 工具方法,底层是:request.getCookies()
		Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName);
		// 如果用javax.servlet.http.Cookie接受值,就直接返回了
		if (Cookie.class.isAssignableFrom(parameter.getNestedParameterType())) {
			return cookieValue;
		} else if (cookieValue != null) { // 否则返回cookieValue
			return this.urlPathHelper.decodeRequestString(servletRequest, cookieValue.getValue());
		} else {
			return null;
		}
	}
}

例子

    @ResponseBody
    @GetMapping("/test")
    public Object test(@CookieValue("JSESSIONID") Cookie cookie,
                       @CookieValue("JSESSIONID") String cookieValue) {
        System.out.println(cookie);
        System.out.println(cookieValue);
        return cookieValue;
    }

手动设置一个cookie值,然后请求

控制台打印如下:

javax.servlet.http.Cookie@401ef395
123456

在现在restful风格下,cookie使用得是很少的了。一般用于提升用户体验方面

MatrixVariableMethodArgumentResolver

标注有@MatrixVariable注解的参数的处理器。Matrix:矩阵,这个注解是Spring3.2新提出来的,增强Restful的处理能力(配合@PathVariable使用),比如这类URL的解析就得靠它:/owners/42;q=11/pets/21;s=23;q=22。

// @since 3.2
public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	// @MatrixVariable注解是必须的。然后技能处理普通类型,也能处理Map
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if (!parameter.hasParameterAnnotation(MatrixVariable.class)) {
			return false;
		}
		if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
			MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class);
			return (matrixVariable != null && StringUtils.hasText(matrixVariable.name()));
		}
		return true;
	}
	...
}

参考学习:https://developer.aliyun.com/article/754099

ExpressionValueMethodArgumentResolver

它用于处理标注有@Value注解的参数。对于这个注解我们太熟悉不过了,没想到在web层依旧能发挥作用。本文就重点来看看它

通过@Value让我们在配置文件里给参数赋值,在某些特殊场合(比如前端不用传,但你想给个默认值,这个时候用它也是一种方案)

说明:这就相当于在Controller层使用了@Value注解,其实我是不太建议的。因为@Value建议还是只使用在业务层

// @since 3.1
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	// 唯一构造函数  支持占位符、SpEL
	public ExpressionValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
		super(beanFactory);
	}

	//必须标注有@Value注解
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(Value.class);
	}

	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		Value ann = parameter.getParameterAnnotation(Value.class);
		return new ExpressionValueNamedValueInfo(ann);
	}
	private static final class ExpressionValueNamedValueInfo extends NamedValueInfo {
		// 这里name传值为固定值  因为只要你的key不是这个就木有问题
		// required传固定值false
		// defaultValue:取值为annotation.value() --> 它天然支持占位符和SpEL嘛
		private ExpressionValueNamedValueInfo(Value annotation) {
			super("@Value", false, annotation.value());
		}
	}

	// 这里恒返回null,因此即使你的key是@Value,也是不会采纳你的传值的哟~
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
		// No name to resolve
		return null;
	}
}

根本原理其实只是利用了defaultValue支持占位符和SpEL的特性而已。给个使用示例:

// 在MVC子容器中导入外部化配置
@Configuration
@PropertySource("classpath:my.properties") // 此处有键值对:test.myage = 18
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter { ... }

    @ResponseBody
    @GetMapping("/test")
    public Object test(@Value("#{T(Integer).parseInt('${test.myage:10}') + 10}") Integer myAge) {
        System.out.println(myAge);
        return myAge;
    }

请求:/test,打印:28

注意:若你写成@Value("#{'${test.myage:10}' + 10},那你得到的答案是:1810(成字符串拼接了)

另外,看到有不少人说如果把这个@PropertySource("classpath:my.properties")放在根容器的config文件里导入,controller层就使用@Value/占位符获取不到值了,其实这是不正确的。理由如下:

Spring MVC子容器在创建时:initWebApplicationContext()

if (cwac.getParent() == null) {
	cwac.setParent(rootContext); // 设置上父容器(根容器)
}

AbstractApplicationContext:如下代码
	// 相当于子容器的环境会把父容器的Enviroment合并进来
	@Override
	public void setParent(@Nullable ApplicationContext parent) {
		this.parent = parent;
		if (parent != null) {
			Environment parentEnvironment = parent.getEnvironment();
			if (parentEnvironment instanceof ConfigurableEnvironment) {
				getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
			}
		}
	}
	
AbstractEnvironment:merge()方法如下
	@Override
	public void merge(ConfigurableEnvironment parent) {
		// 完全的从parent里所有的PropertySources里拷贝一份进来
		for (PropertySource<?> ps : parent.getPropertySources()) {
			if (!this.propertySources.contains(ps.getName())) {
				this.propertySources.addLast(ps);
			}
		}
		...	
	}

这就是为什么说即使你是在根容器里使用的@PropertySource导入的外部资源,子容器也可以使用的原因(因为子容器会把父环境给merge一份过来)。

但是,但是,但是:如果你是使用形如PropertyPlaceholderConfigurer这种方式导进来的,那是会有容器隔离效应的


参数类型是Map的

数据来源同上,只是参数类型是Map

这类解析器我认为是对第一类的有些处理器的一种补充,它依赖上面的相关注解。

你是否想过通过@RequestParam一次性全给封装进一个Map里,然后再自己分析?同样的本类处理器给@RequestHeader、@PathVariable、@MatrixVariable都赋予了这种能力

PathVariableMapMethodArgumentResolver

// @since 3.2 晚一个版本号
public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver {

	// 必须标注@PathVariable注解  并且类型是Map,并且注解不能有value值
	// 处理情况和PathVariableMethodArgumentResolver形成了互补
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
		return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
				!StringUtils.hasText(ann.value()));
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		... // 处理上极其简单,把所有的路径参数使用Map装着返回即可
	}
}

RequestParamMapMethodArgumentResolver

它依赖的方法是:HttpServletRequest#getParameterMap()、MultipartRequest#getMultiFileMap()、MultipartRequest#getFileMap()等,出现于Spring 3.1。

    @ResponseBody
    @GetMapping("/test")
    public Object test(@RequestParam Map<String,Object> params) {
        System.out.println(params);
        return params;
    }

请求:/test?name=fsx&age=18&age=28。打印

{name=fsx, age=18}

从结果看出:

  1. 它不能传一key多值情况
  2. 若出现相同的key,以在最前面的key的值为准。
  3. Map实例是一个LinkedHashMap<String,String>实例

RequestHeaderMapMethodArgumentResolver

一次性把请求头信息都拿到:数据类型支出写MultiValueMap(LinkedMultiValueMap)/HttpHeaders/Map。实例如下:

    @ResponseBody
    @GetMapping("/test")
    public Object test(@RequestHeader Map<String, Object> headers) {
        headers.forEach((k, v) -> System.out.println(k + "-->" + v));
        return headers;
    }

请求打印:

host-->localhost:8080
connection-->keep-alive
cache-control-->max-age=0
upgrade-insecure-requests-->1
user-agent-->Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
sec-fetch-mode-->navigate
sec-fetch-user-->?1
accept-->text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
sec-fetch-site-->none
accept-encoding-->gzip, deflate, br
accept-language-->zh-CN,zh;q=0.9
cookie-->JSESSIONID=123456789

不过强烈不建议直接使用Map,而是使用HttpHeaders类型。这么写@RequestHeader HttpHeaders headers,获取的时候更为便捷。

MatrixVariableMapMethodArgumentResolver

PASS,略过

MapMethodProcessor

它处理Map类型,但没有标注任何注解的情况,它的执行顺序是很靠后的,所以有点兜底的意思。

// @since 3.1
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return Map.class.isAssignableFrom(parameter.getParameterType());
	}

	// 处理逻辑非常简单粗暴:把Model直接返回~~~~
	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		return mavContainer.getModel();
	}
}

这个处理器同时也解释了:为何你方法入参上写个Map、HashMap、ModelMap等等就可以非常便捷的获取到模型的值的原因

固定参数类型

参数比如是SessionStatus, ServletResponse, OutputStream, Writer, WebRequest, MultipartRequest, HttpSession, Principal, InputStream等

这种方式使用得其实还比较多的。比如平时我们需要用Servlet源生的API:HttpServletRequest, HttpServletResponse肿么办? 在Spring MVC内就特别特别简单,只需要在入参上声明:就可以直接使用啦~

ServletRequestMethodArgumentResolver

// 它支持到的可不仅仅是ServletRequest,多到令人发指
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	// 连Servlet 4.0的PushBuilder都支持了(Spring5.0以上版本支持的)
	@Nullable
	private static Class<?> pushBuilder;
	static {
		try {
			pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
					ServletRequestMethodArgumentResolver.class.getClassLoader());
		} catch (ClassNotFoundException ex) {
			// Servlet 4.0 PushBuilder not found - not supported for injection
			pushBuilder = null;
		}
	}

	// 支持"注入"的类型,可谓多多益善
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (WebRequest.class.isAssignableFrom(paramType) ||
				ServletRequest.class.isAssignableFrom(paramType) || // webRequest.getNativeRequest(requiredType)
				MultipartRequest.class.isAssignableFrom(paramType) ||
				HttpSession.class.isAssignableFrom(paramType) || //request.getSession()
				(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || //PushBuilderDelegate.resolvePushBuilder(request, paramType);
				Principal.class.isAssignableFrom(paramType) || //request.getUserPrincipal()
				InputStream.class.isAssignableFrom(paramType) || // request.getInputStream()
				Reader.class.isAssignableFrom(paramType) || //request.getReader()
				HttpMethod.class == paramType || //HttpMethod.resolve(request.getMethod());
				Locale.class == paramType || //RequestContextUtils.getLocale(request)
				TimeZone.class == paramType || //RequestContextUtils.getTimeZone(request)
				ZoneId.class == paramType); //RequestContextUtils.getTimeZone(request);
	}
}

看到这你应该明白,以后你需要使用这些参数的话,直接在方法上申明即可,不需要自己再去get了,又是一种依赖注入的效果体现有没有

ServletResponseMethodArgumentResolver

// @since 3.1
public class ServletResponseMethodArgumentResolver implements HandlerMethodArgumentResolver {
	// 它相对来说很比较简单
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (ServletResponse.class.isAssignableFrom(paramType) || // webRequest.getNativeResponse(requiredType)
				OutputStream.class.isAssignableFrom(paramType) || //response.getOutputStream()
				Writer.class.isAssignableFrom(paramType)); //response.getWriter()
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		// 这个判断放在这。。。
		if (mavContainer != null) {
			mavContainer.setRequestHandled(true);
		}
		... 
	}
}

SessionStatusMethodArgumentResolver

支持SessionStatus。值为:mavContainer.getSessionStatus();

UriComponentsBuilderMethodArgumentResolver

// @since 3.1
public class UriComponentsBuilderMethodArgumentResolver implements HandlerMethodArgumentResolver {
	// UriComponentsBuilder/ ServletUriComponentsBuilder
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> type = parameter.getParameterType();
		return (UriComponentsBuilder.class == type || ServletUriComponentsBuilder.class == type);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
		return ServletUriComponentsBuilder.fromServletMapping(request);
	}
}

通过UriComponentsBuilder来得到URL的各个部分,以及构建URL都是非常的方便的。

RedirectAttributesMethodArgumentResolver

和重定向属性RedirectAttributes相关。

// @since 3.1
public class RedirectAttributesMethodArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return RedirectAttributes.class.isAssignableFrom(parameter.getParameterType());
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		ModelMap redirectAttributes;

		// 把DataBinder传入到RedirectAttributesModelMap里面去~~~~
		if (binderFactory != null) {
			DataBinder dataBinder = binderFactory.createBinder(webRequest, null, DataBinder.DEFAULT_OBJECT_NAME);
			redirectAttributes = new RedirectAttributesModelMap(dataBinder);
		} else {
			redirectAttributes  = new RedirectAttributesModelMap();
		}
		mavContainer.setRedirectModel(redirectAttributes);
		return redirectAttributes;
	}
}

如果涉及到重定向:多个视图见传值,使用它还是比较方便的。

ModelMethodProcessor

允许你入参里写:org.springframework.ui.Model、RedirectAttributes、RedirectAttributesModelMap、ConcurrentModel、ExtendedModelMap等等


这里说一个特殊的处理器:ModelAttributeMethodProcessor:主要是针对 被 @ModelAttribute注解修饰且不是普通类型(通过 !BeanUtils.isSimpleProperty来判断)的参数。

// @since 3.1
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	// 标注有@ModelAttribute它会处理
	// 若没有标注(只要不是“简单类型”),它也会兜底处理
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
	}
}

基于ContentType消息转换器类型

我们使用非常频繁的@RequestBody是怎么封装请求体的呢?

利用HttpMessageConverter将输入流转换成对应的参数

这类参数解析器的基类是AbstractMessageConverterMethodArgumentResolver:

// @since 3.1
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {

	// 默认支持的方法(没有Deleted方法)
	// httpMethod为null 或者方法不属于这集中 或者没有contendType且没有body 那就返回null
	// 也就是说如果是Deleted请求,即使body里有值也是返回null的。(因为它不是SUPPORTED_METHODS )
	private static final Set<HttpMethod> SUPPORTED_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
	private static final Object NO_VALUE = new Object();

	protected final List<HttpMessageConverter<?>> messageConverters;
	protected final List<MediaType> allSupportedMediaTypes;
	// 和RequestBodyAdvice和ResponseBodyAdvice有关的
	private final RequestResponseBodyAdviceChain advice;

	// 构造函数里指定HttpMessageConverter
	// 此一个参数的构造函数木人调用
	public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
		this(converters, null);
	}

	// @since 4.2
	public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters, @Nullable List<Object> requestResponseBodyAdvice) {
		Assert.notEmpty(converters, "'messageConverters' must not be empty");
		this.messageConverters = converters;
		// 它会把所有的消息转换器里支持的MediaType都全部拿出来汇聚起来~
		this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
		this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
	}

	// 提供一个defualt方法访问
	RequestResponseBodyAdviceChain getAdvice() {
		return this.advice;
	}

	// 子类RequestResponseBodyMethodProcessor有复写此方法
	@Nullable
	protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

		HttpInputMessage inputMessage = createInputMessage(webRequest);
		return readWithMessageConverters(inputMessage, parameter, paramType);
	}
	...
}

说明:此抽象类并没有实现resolveArgument()这个接口方法,而只是提供了一些protected方法,作为工具方法给子类调用,比如最为重要的这个方法:readWithMessageConverters()就是利用消息转换器解析HttpInputMessage的核心。

继承树:

RequestPartMethodArgumentResolver

它用于解析参数被@RequestPart修饰,或者参数类型是MultipartFile | Servlet 3.0提供的javax.servlet.http.Part类型(并且没有被@RequestParam修饰),数据通过 HttpServletRequest获取

当属性被标注为@RequestPart的话,那就会经过HttpMessageConverter结合Content-Type来解析,这个效果特别像@RequestBody的处理方式

// @since 3.1
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPart {
	@AliasFor("name")
	String value() default "";
	@AliasFor("value")
	String name() default "";
	boolean required() default true;
}
// @since 3.1
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

	// 标注了@RequestPart注解的
	// 没有标注@RequestPart并且也没有标注@RequestParam,但是是Multipart类型的也会处理
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if (parameter.hasParameterAnnotation(RequestPart.class)) {
			return true;
		} else {
			if (parameter.hasParameterAnnotation(RequestParam.class)) {
				return false;
			}
			return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
		}
	}

	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
		Assert.state(servletRequest != null, "No HttpServletRequest");

		RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
		boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());

		// 如果注解没有指定,就取形参名
		String name = getPartName(parameter, requestPart);
		parameter = parameter.nestedIfOptional();
		Object arg = null;

		// resolveMultipartArgument这个方法只处理:
		// MultipartFile类型以及对应的数组/集合类型
		// Part类型以及对应的数组集合类型
		// 若形参类型不是以上类型,返回UNRESOLVABLE(空对象)

		// 最终返回StandardMultipartHttpServletRequest/request.getParts()[0]等~
		Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
		if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
			arg = mpArg; // 是part类型,那就直接赋值吧
		} else { // 其它类型
			...
		}
		...
	}
}

此处理器用于解析@RequestPart参数类型,它和多部分文件上传有关。关于Spring MVC中的文件上传,此处就不便展开了。后面有个专题专门讲解Spring MVC中的上传、下载

AbstractMessageConverterMethodProcessor(重点)

命名为Processor说明它既能处理入参,也能处理返回值,当然本文的关注点是方法入参(和HttpMessageConverter相关)

请求body体一般是一段字符串/字节流,查询参数可以看做URL的一部分,这两个是位于请求报文的不同地方。

表单参数可以按照一定格式放在请求体中,也可以放在url上作为查询参数。

响应body体则是response返回的具体内容,对于一个普通的html页面,body里面就是页面的源代码。对于HttpMessage响应体里可能就是个json串(但无强制要求)。

响应体一般都会结合Content-Type一起使用,告诉客户端只有知道这个头了才知道如何渲染。

AbstractMessageConverterMethodProcessor源码稍显复杂,它和Http协议、内容协商有很大的关联:

// @since 3.1
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {

	// 默认情况下:文件们后缀是这些就不弹窗下载
	private static final Set<String> WHITELISTED_EXTENSIONS = new HashSet<>(Arrays.asList("txt", "text", "yml", "properties", "csv",
			"json", "xml", "atom", "rss", "png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp"));
	private static final Set<String> WHITELISTED_MEDIA_BASE_TYPES = new HashSet<>(Arrays.asList("audio", "image", "video"));
	private static final List<MediaType> ALL_APPLICATION_MEDIA_TYPES = Arrays.asList(MediaType.ALL, new MediaType("application"));
	private static final Type RESOURCE_REGION_LIST_TYPE = new ParameterizedTypeReference<List<ResourceRegion>>() { }.getType();
	
	// 用于给URL解码 decodingUrlPathHelper.decodeRequestString(servletRequest, filename);
	private static final UrlPathHelper decodingUrlPathHelper = new UrlPathHelper();
	// rawUrlPathHelper.getOriginatingRequestUri(servletRequest);
	private static final UrlPathHelper rawUrlPathHelper = new UrlPathHelper();
	static {
		rawUrlPathHelper.setRemoveSemicolonContent(false);
		rawUrlPathHelper.setUrlDecode(false);
	}

	// 内容协商管理器
	private final ContentNegotiationManager contentNegotiationManager;
	// 扩展名的内容协商策略
	private final PathExtensionContentNegotiationStrategy pathStrategy;
	private final Set<String> safeExtensions = new HashSet<>();

	protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) {
		this(converters, null, null);
	}
	// 可以指定内容协商管理器ContentNegotiationManager 
	protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters, @Nullable ContentNegotiationManager contentNegotiationManager) {
		this(converters, contentNegotiationManager, null);
	}
	// 这个构造器才是重点
	protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters, @Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
		super(converters, requestResponseBodyAdvice);

		// 可以看到:默认情况下会直接new一个
		this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
		// 若管理器里有就用管理器里的,否则new PathExtensionContentNegotiationStrategy()
		this.pathStrategy = initPathStrategy(this.contentNegotiationManager);

		// 用safeExtensions装上内容协商所支持的所有后缀
		// 并且把后缀白名单也加上去(表示是默认支持的后缀)
		this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
		this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
	}

	// ServletServerHttpResponse是对HttpServletResponse的包装,主要是对响应头进行处理
	// 主要是处理:setContentType、setCharacterEncoding等等
	// 所以子类若要写数据,就调用此方法来向输出流里写吧~~~
	protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
		HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
		Assert.state(response != null, "No HttpServletResponse");
		return new ServletServerHttpResponse(response);
	}

	// 注意:createInputMessage()方法是父类提供的,对HttpServletRequest的包装
	// 主要处理了:getURI()、getHeaders()等方法
	// getHeaders()方法主要是处理了:getContentType()...


	protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
		writeWithMessageConverters(value, returnType, inputMessage, outputMessage);
	}

	// 这个方法省略
	// 这个方法是消息处理的核心之核心:处理了contentType、消息转换、内容协商、下载等等
	// 注意:此处并且还会执行RequestResponseBodyAdviceChain,进行前后拦截
	protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { ... }
}

本类的核心是各式各样的HttpMessageConverter消息转换器,因为最终的write都是交给它们去完成。

此抽象类里,它完成了内容协商

既然父类都已经完成了这么多事,那么子类自然就非常的简单的。看看它的两个具体实现子类:

RequestResponseBodyMethodProcessor

顾名思义,它负责处理@RequestBody这个注解的参数

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(RequestBody.class);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		// 所以核心逻辑:读取流、消息换换等都在父类里已经完成。子类直接调用就可以拿到转换后的值arg 
		// arg 一般都是个类对象。比如Person实例
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		// 若是POJO,就是类名首字母小写(并不是形参名)
		String name = Conventions.getVariableNameForParameter(parameter);

		// 进行数据校验(之前已经详细分析过,此处一笔带过)
		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}

			// 把校验结果放进Model里,方便页面里获取
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		// 适配:支持到Optional类型的参数
		return adaptArgumentIfNecessary(arg, parameter);
	}
}

HttpEntityMethodProcessor

用于处理HttpEntity和RequestEntity类型的入参的。

public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (HttpEntity.class == parameter.getParameterType() || RequestEntity.class == parameter.getParameterType());
	}

	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws IOException, HttpMediaTypeNotSupportedException {

		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		// 拿到HttpEntity的泛型类型
		Type paramType = getHttpEntityType(parameter);
		if (paramType == null) {
			// 注意:这个泛型类型是必须指定的,必须的
			throw new IllegalArgumentException("HttpEntity parameter '" + parameter.getParameterName() + "' in method " + parameter.getMethod() + " is not parameterized");
		}

		// 调用父类方法拿到body的值(把泛型类型传进去了,所以返回的是个实例)
		Object body = readWithMessageConverters(webRequest, parameter, paramType);
		// 注意步操作:new了一个RequestEntity进去,持有实例即可
		if (RequestEntity.class == parameter.getParameterType()) {
			return new RequestEntity<>(body, inputMessage.getHeaders(), inputMessage.getMethod(), inputMessage.getURI());
		} else { // 用的父类HttpEntity,那就会丢失掉Method等信息(因此建议入参用RequestEntity类型,更加强大些)
			return new HttpEntity<>(body, inputMessage.getHeaders());
		}
	}
}

注意:这里可没有validate校验了,这也是经常被面试问到的:使用HttpEntity和@RequestBody有什么区别呢?

有了抽象父类后,子类需要做的事情已经很少了,只需要匹配参数类型、做不同的返回而已。

  • @RequestBody/HttpEntity它的参数(泛型)类型允许是Map
  • 方法上的和类上的@ResponseBody都可以被继承,但@RequestBody不可以
  • @RequestBody它自带有Bean Validation校验能力(当然需要启用),HttpEntity更加的轻量和方便

HttpEntity/RequestEntity所在包是:org.springframework.http,属于spring-web

@RequestBody位于org.springframework.web.bind.annotation,同样属于spring-web

最后还落了一个ErrorsMethodArgumentResolver,在这里补充一下:

ErrorsMethodArgumentResolver

它用于在方法参数可以写Errors类型,来拿到数据校验结果。

public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return Errors.class.isAssignableFrom(paramType);
	}

	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter,
			@Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
			@Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null,
				"Errors/BindingResult argument only supported on regular handler methods");

		ModelMap model = mavContainer.getModel();
		String lastKey = CollectionUtils.lastElement(model.keySet());
		
		// 只有@RequestBody/@RequestPart注解的  这里面才会有值
		if (lastKey != null && lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
			return model.get(lastKey);
		}

		// 简单的说:必须有@RequestBody/@RequestPart这注解标注,Errors参数才有意义
		throw new IllegalStateException(
				"An Errors/BindingResult argument is expected to be declared immediately after " +
				"the model attribute, the @RequestBody or the @RequestPart arguments " +
				"to which they apply: " + parameter.getMethod());
	}
}

Spring MVC参数处理器的注册与顺序

到这里,一个不落的把Spring MVC内置提供的参数处理器ArgumentResolver说了个遍。

前面我有提到过:参数处理对处理器的顺序是敏感的,因此我们需要关注Spring MVC最终的执行顺序,这时候我们的聚合容器HandlerMethodArgumentResolverComposite就出场了:

public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

	private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
	// 具有缓存
	private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);	
	...
	// @since 4.3  木有任何地方调用
	public void clear() {
		this.argumentResolvers.clear();
	}

	// getArgumentResolver()方法是本文的核心
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return getArgumentResolver(parameter) != null;
	}
	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		
		// 这里是关键:每个参数最多只会被一个处理器处理
		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
		if (resolver == null) {
			throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]." + " supportsParameter should be called first.");
		}
		return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
	}
	
	...	

	// 这块逻辑保证了每个parameter参数最多只会被一个处理器处理
	// 这个从缓存的数据结构中也能够看出来的
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
				if (methodArgumentResolver.supportsParameter(parameter)) {
					result = methodArgumentResolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}
}

缺省情况Spring MVC注册的处理器(顺序)如下:

它初始化处的代码如下:

RequestMappingHandlerAdapter:
	@Override
	public void afterPropertiesSet() {
		...
		// 26个,详见方法getDefaultArgumentResolvers
		if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		// 12个  详见方法getDefaultInitBinderArgumentResolvers
		if (this.initBinderArgumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
			this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		...
	}

注意:这里面initBinderArgumentResolvers最终只会有12个处理器,因为它的注册方法如下截图(也是这个顺序):

PS:这里是13个处理器,新的Spring5.3多了一个

resolvers.add(new PrincipalMethodArgumentResolver());

是权限框架获取用户主体信息的,不影响

前面有提到过说标注有@InitBInder注解里也可以写很多类型的参数,但因为它只会有12个处理器,所以有些参数它是不能写的(比如@RequestBody、Errors等等这种都是不能写的),不用一一枚举,做到心中有数就成。


自定参数解析器处理特定场景需求

前面介绍了HandlerMethodArgumentResolver这个参数解析器以及它的所有内置实现,相信看过的人对它的加载、初始化、处理原理等等已能够做到了心中有数了。

Spring MVC内置注册了灰常多的处理器给我们的使用,不客气说几乎100%的case我们都是足够用了的。但既然我们已经理解到了HandlerMethodArgumentResolver它深层的作用原理,那么本文就通过自定义参数处理器,来做到屏蔽(隔离)基础实现、更高效的编写业务编码。

关于它的应用场景可以非常多,本文我总结出最为常见、好理解的两个应用场景作为举例说明:

  1. 获取当前登陆人(当然用户)的基本信息
  2. 调整(兼容)数据结构

场景一:

在Controller层获取当前登陆人的基本信息(如id、名字…)是一个必须的、频繁的功能需求,这个时候如果团队内没有提供相关封装好的方法来调用,你便可看到大量的、重复的获取当前用户的代码,这就是各位经常吐槽的垃圾代码

一般团队的做法是:提供BaseController,在基类里面提供获取当前用户的功能方法,这样业务控制器Controller只需要继承它就有这能力了,使用起来确实也还挺方便的。但是是否还思考过这种通过继承的方式它是有弊端的–>我只想获取当前登陆人我就得继承一个父类?这是不是设计太重了点?更坏的情况是如果此时我已经有父类了呢?

面对我提出的问题,本文针对性的提供一个新的、更加轻量的解决思路:自定义HandlerMethodArgumentResolver来实现获取当前登录用户的解决方案。实施步骤如下:

自定义一个参数注解(注解并不是100%必须的,可完全根据类型来决策)

/**
 * 用于获取当前登陆人信息的注解,配合自定义的参数处理器使用
 *
 * @see CurrUserArgumentResolver
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrUser {
}

// 待封装的Vo
@Getter
@Setter
@ToString
public class CurrUserVo {
    private Long id;
    private String name;
}

自定义参数解析器CurrUserArgumentResolver并完成注册

public class CurrUserArgumentResolver implements HandlerMethodArgumentResolver {

    // 只有标注有CurrUser注解,并且数据类型是CurrUserVo/Map/Object的才给与处理
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        CurrUser ann = parameter.getParameterAnnotation(CurrUser.class);
        Class<?> parameterType = parameter.getParameterType();
        return (ann != null &&
                (CurrUserVo.class.isAssignableFrom(parameterType)
                        || Map.class.isAssignableFrom(parameterType)
                        || Object.class.isAssignableFrom(parameterType)));
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        // 从请求头中拿到token
        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            return null; // 此处不建议做异常处理,因为校验token的事不应该属于它来做,别好管闲事
        }

        // 此处作为测试:new一个处理(写死的)
        CurrUserVo userVo = new CurrUserVo();
        userVo.setId(1L);
        userVo.setName("fsx");

        // 判断参数类型进行返回
        Class<?> parameterType = parameter.getParameterType();
        if (Map.class.isAssignableFrom(parameterType)) {
            Map<String, Object> map = new HashMap<>();
            BeanUtils.copyProperties(userVo, map);
            return map;
        } else {
            return userVo;
        }

    }
}

// 注册进Spring组件内
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new CurrUserArgumentResolver());
    }
}

测试例子

@Controller
@RequestMapping
public class HelloController {

    @ResponseBody
    @GetMapping("/test/curruser")
    public Object testCurrUser(@CurrUser CurrUserVo currUser) {
        return currUser;
    }
    @ResponseBody
    @GetMapping("/test/curruser/map")
    public Object testCurrUserMap(@CurrUser Map<String,Object> currUser) {
        return currUser;
    }
    @ResponseBody
    @GetMapping("/test/curruser/object")
    public Object testCurrUserObject(@CurrUser Object currUser) {
        return currUser;
    }
}

请求:/test/curruser或者/test/curruser/object 这两个请求得到的答案是一致的且符合预期,结果如下截图:

但是,但是,但是若访问/test/curruser/map,它的结果如下:

so参数类型是Map类型,自定义的参数解析器CurrUserArgumentResolver并没有生效,为什么呢?

接下来我说说对此非常重要的使用细节

如何使用Spring容器内的Bean?

在本例中,为了方便,我在CurrUserArgumentResolver里写死的自己new的一个CurrUserVo作为返回。实际应用场景中,此部分肯定是需要根据token去访问DB/Redis的,因此就需要使用到Spring容器内的Bean。

有的人就想当然了,在本例上直接使用@Autowired HelloService helloService;来使用,经测试发现这是注入不进来的,helloService值为null。那么本文就教你正确的使用姿势:

姿势一:把自定义的参数解析器也放进容器

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Bean
    public CurrUserArgumentResolver currUserArgumentResolver(){
        return new CurrUserArgumentResolver();
    }
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(currUserArgumentResolver());
    }

这样,你在CurrUserArgumentResolver就可以顺理成章的注入想要的组件了,形如这样:

public class CurrUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    HelloService helloService;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    ...
}

这种方案的优点是:在Spring容器内它几乎能解决大部分类似问题,在组件不是很多的情况下,推荐新手使用,因为无需过多的理解Spring内部机制便可轻松使用。

姿势二:借助AutowireCapableBeanFactory给对象赋能

本着"减轻"Spring容器"负担"的目的,"手动"精细化控制Spring内的Bean组件。像本文的这种解析器其实是完全没必要放进容器内的,需要什么组件让容器帮你完成注入即可,自己本文就没必要放进去喽:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private ApplicationContext applicationContext;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        CurrUserArgumentResolver resolver = new CurrUserArgumentResolver();
        // 利用工厂给容器外的对象注入所需组件
        applicationContext.getAutowireCapableBeanFactory().autowireBean(resolver);
        argumentResolvers.add(resolver);
    }
}

本姿势的技巧是利用了AutowireCapableBeanFactory巧妙完成了给外部对象赋能,从而即使自己并不是容器内的Bean,也能自由注入、使用容器内Bean的能力(同样可以随意使用@Autowired注解了)。

这种方式是侵入性最弱的,是我推荐的方式。当然这需要你对Spring容器有一定的了解才能运用自如,做到心中有数才行,否则不建议你使用

可以和内置的一些注解/类型一起使用吗?(参数类型是Map类型?)

作为一个"合格"的coder,理应发出如题这样的疑问。

譬如上例我这么写,你可以猜猜是什么结果:

@ResponseBody
@GetMapping("/test/curruser")
public Object testCurrUser(@CurrUser @RequestParam CurrUserVo currUser) {
    return currUser;
}

表面上看起来没有毛病,但请求:/test/curruser?currUser=fsx。报错如下:

Resolved [org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.fsx.validator.CurrUserVo'; 
nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.fsx.validator.CurrUserVo': no matching editors or conversion strategy found]

调试源码可以发现它最终使用的参数解析器是:RequestParamMethodArgumentResolver,而并非我们自定义的CurrUserArgumentResolver。so可得出结论:我们自定义的参数解析器的优先级是低于Spring内置的。

那么到底是什么样的优先级规则呢?

首先就得从RequestMappingHandlerAdapter说起,它对参数解析器的加载(初始化)顺序:

RequestMappingHandlerAdapter:
	@Override
	public void afterPropertiesSet() {
		// 显然,也是允许你自己通过setArgumentResolvers()方法手动添加的~~~
		// 加入你调用过set方法,这里就不会执行啦~~~~~(一般不建议手动set)
		if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		... 
	}

	private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

		// Annotation-based argument resolution
		// 加载处理所有内置注解的解析器们
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
		resolvers.add(new RequestParamMapMethodArgumentResolver());
		resolvers.add(new PathVariableMethodArgumentResolver());
		...

		// Type-based argument resolution
		// 比如request、response等等这些的解析器们
		resolvers.add(new ServletRequestMethodArgumentResolver());
		...
		
		// Custom arguments 
		// 加载自定义的解析器们(我们自定义的在这里会被加载进来)
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		// 加载这两个用于兜底
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
		resolvers.add(new ServletModelAttributeMethodProcessor(true));

		return resolvers;
	}

RequestMappingHandlerAdapter这个Bean配置处如下

WebMvcConfigurationSupport:
	@Bean
	public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
		RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
		// 内容协商管理器
		adapter.setContentNegotiationManager(mvcContentNegotiationManager());
		// 消息转换器们
		adapter.setMessageConverters(getMessageConverters());
		// ConfigurableWebBindingInitializer:配置数据绑定、校验的相关配置项
		adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
		// 参数解析器、返回值解析器
		adapter.setCustomArgumentResolvers(getArgumentResolvers());
		adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
		...
	}

WebMvcConfigurationSupport应该没有不熟悉它的了,它用于开启WebMVC的配置支持

为何本例加了@RequestParam注解就访问就报错了;同样也解释了为何入参不能是Map(但Object类型是可以)。

在介绍场景二之前,我先介绍一个类:PropertyNamingStrategy

PropertyNamingStrategy

它表示序列化/反序列化过程中:Java属性到序列化key的一种命名策略。

默认情况下从字符串反序列为一个Java对象,要求需要完全一样才能反序列赋值成功。但了解了这些策略之后,可以帮你带来更好的兼容性,下面以最为常用的两个JSON库为例分别讲解

Gson库对应的类叫FieldNamingStrategy,功能类似。

fastjson

fastjson在1.2.15版本(2016年6月)中提供了这个功能,它以枚举的形式管理:

public enum PropertyNamingStrategy {
	CamelCase, // 骆驼:
	PascalCase, // 帕斯卡:
	SnakeCase, // 蛇形:
	KebabCase; // 烤肉串:

	// 提供唯一一个实例方法:转换translate
    public String translate(String propertyName) {
	    switch (this) {
	    	case SnakeCase: { ... }
	    	case KebabCase: { ... }
	    	case PascalCase: { ... }
	    	case CamelCase: { ... }
	    }
    }
}

针对此4种策略,给出使用用例如下:

public static void main(String[] args)  {
    String propertyName = "nameAndAge";
    System.out.println(PropertyNamingStrategy.CamelCase.translate(propertyName)); // nameAndAge
    System.out.println(PropertyNamingStrategy.PascalCase.translate(propertyName)); // NameAndAge
    // 下面两种的使用很多的情况:下划线
    System.out.println(PropertyNamingStrategy.SnakeCase.translate(propertyName)); // name_and_age
    System.out.println(PropertyNamingStrategy.KebabCase.translate(propertyName)); // name-and-age
}

继续演示使用Fastjson序列化/反序列化的时候的示例:

public static void main(String[] args) {
    DemoVo vo = new DemoVo();
    vo.setDemoName("fsx");
    vo.setDemoAge(18);
    vo.setDemoNameAndAge("fsx18");

    PropertyNamingStrategy strategy = PropertyNamingStrategy.SnakeCase;
    // 序列化配置对象
    SerializeConfig config = new SerializeConfig();
    config.propertyNamingStrategy = strategy;
    // 反序列化配置对象
    ParserConfig parserConfig = new ParserConfig();
    parserConfig.propertyNamingStrategy = strategy;

    // 序列化对象
    String json = JSON.toJSONString(vo, config);
    System.out.println("序列化vo对象到json -> " + json);

    // 反序列化对象
    vo = JSON.parseObject(json, DemoVo.class, parserConfig);
    System.out.println("反序列化json到vo -> " + vo);
}

运行打印:

序列化vo对象到json -> {"demo_age":18,"demo_name":"fsx","demo_name_and_age":"fsx18"}
反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)

若策略是SnakeCase,它是支持下划线_到驼峰格式的Java属性的相互转换的。若使用另外三种,我把结果摘录如下:

CamelCase:
序列化vo对象到json -> {"demoAge":18,"demoName":"fsx","demoNameAndAge":"fsx18"}
反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)

PascalCase:
序列化vo对象到json -> {"DemoAge":18,"DemoName":"fsx","DemoNameAndAge":"fsx18"}
反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)

KebabCase:
序列化vo对象到json -> {"demo-age":18,"demo-name":"fsx","demo-name-and-age":"fsx18"}
反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)

FastJson默认使用CamelCase

全局指定策略

SerializeConfig.getGlobalInstance().propertyNamingStrategy = PropertyNamingStrategy.PascalCase;

@JSONType指定

@JSONType(naming = PropertyNamingStrategy.SnakeCase)
private static class DemoVo {
    @JSONField(name = "name")
    private String demoName;
    private Integer demoAge;
    private Object demoNameAndAge;
}

若@JSONField没有指定name属性,那就会使用PropertyNamingStrategy策略

jackson

除了fastjson,作为全球范围内更为流行的jackson自然也是支持此些策略的。

// was abstract until 2.7 在2.7版本之前一直是抽象类
public class PropertyNamingStrategy implements java.io.Serializable {
    public static final PropertyNamingStrategy SNAKE_CASE = new SnakeCaseStrategy();
    public static final PropertyNamingStrategy UPPER_CAMEL_CASE = new UpperCamelCaseStrategy();
    public static final PropertyNamingStrategy LOWER_CAMEL_CASE = new PropertyNamingStrategy();
    public static final PropertyNamingStrategy KEBAB_CASE = new KebabCaseStrategy();
	
	// 上面几个策略都是@since 2.7,这个基于@since 2.4
    public static final PropertyNamingStrategy LOWER_CASE = new LowerCaseStrategy();

 	// 提供的API方法如下:
	public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName);
    public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName);
    public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName);
    public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName);

	// 所有策略都使用静态内部类来实现(只需要实现translate方法即可)
	public static class SnakeCaseStrategy extends PropertyNamingStrategyBase
	public static class UpperCamelCaseStrategy extends PropertyNamingStrategyBase
	...
}

下面结合它的注解@JsonNaming来演示它的使用:

@Getter
@Setter
@ToString
// 此注解只能标注在类上
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
private static class DemoVo {
    private String demoName;
    private Integer demoAge;
    @JsonProperty("diyProp")
    private Object demoNameAndAge;
}

public static void main(String[] args) throws IOException {
    DemoVo vo = new DemoVo();
    vo.setDemoName("fsx");
    vo.setDemoAge(18);
    vo.setDemoNameAndAge("fsx18");

    // 序列化对象
    ObjectMapper objectMapper = new ObjectMapper();
    String json = objectMapper.writeValueAsString(vo);
    System.out.println("序列化vo对象到json -> " + json);

    // 反序列化对象
    vo = objectMapper.readValue(json,DemoVo.class);
    System.out.println("反序列化json到vo -> " + vo);
}

打印输出结果:

序列化vo对象到json -> {"demo_name":"fsx","demo_age":18,"diyProp":"fsx18"}
反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)

显然基于字段的注解@JsonProperty它的优先级是高于@JsonNaming的

除此之外,jackson还提供了更多实用注解,可以自行去了解

jackson可能是由于功能设计得太过于全面了,使用起来有反倒很多不便之处,学习成本颇高。因为个人觉得还是我天朝的Fastjson好用啊

场景二:

在微服务场景中有个特别常见的现象:跟第三方服务做对接时(如python老系统),你不乏会遇到如下两个痛点:

  1. 对方系统是以下划线形式命名的(和Java命名规范相悖)
  2. 对方系统的参数json串层次较深,而对你有用的仅仅是深处的一小部分

例如这个参数串:

{
    "data": {
        "transport_data": {
            "demo_name": "fsx",
            "demo_age": 18
        },
        "secret_info": {
            "code": "fkldshjfkldshj"
        }
    },
    "code": "200",
    "msg": "this is a message"
}

对你真正有用的只有demo_name和demo_age两个值,怎么破???

我相信绝大部分小伙伴都这么做:按照此结构先定义一个DTO全部接收回来(字段命名也用下划线方式命名),然后再一个个处理。

有如下两个不妥的地方:

  1. Java属性名也必须用下划线命名,看起来影响了命名体系(其实就是看着不爽,哈哈)
  2. 按照参数这种复杂结构书写,使得我们关注点分散,不能聚焦到真真关心的那一块数据上

针对这些痛点,废话不多说,直接上处理方案:

定义一个模型(只写我自己关注的属性)

@Getter
@Setter
@ToString
public class TranUserVo {
    private String demoName;
    private Long demoAge;
}

定义的模型非常之简单,不仅只关心我要的数据,而且还是标准的java驼峰命名,没必要去迁就别的语言而丧失自己优雅性,否则容易把自己弄得四不像

自定义一个参数解析器并且注册上去

public class TranUserArgumentResolver implements HandlerMethodArgumentResolver {

    // 只处理这个类型,不需要注解
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();
        return TranUserVo.class.isAssignableFrom(parameterType);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());

        // 本例为了简单,演示get的情况(这里使用key为:demoKey)
        if (httpMethod == HttpMethod.GET) {
            String value = request.getParameter("demoKey");
            JSONObject transportData = (JSONObject) ((JSONObject) JSON.parseObject(value).get("data")).get("transport_data");

            // 采用命名策略,转换TranUserVo实例对象再返回
            // 序列化配置对象
            ParserConfig config = new ParserConfig();
            config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
            TranUserVo tranUserVo = transportData.toJavaObject(TranUserVo.class, config, 0);
            return tranUserVo;
        } else { // 从body提里拿
            // ...
            return null;
        }
    }
}

// 注册此自定义的参数解析器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new TranUserArgumentResolver());
}

对此部分我说明一点:对于json到对象的解析,理应还加上@Valid校验的能力的,此部分我就省略了,毕竟也不是本文所关心的重点

测试

@ResponseBody
@GetMapping("/test/tranuser")
public Object testCurrUser(TranUserVo tranUser) {
    return tranUser;
}

请求:/test/tranuser?demoKey=上面那一大长串json串,得到的结果就是预期的结果喽:

这种长传现在需要使用post/put传递,本文只是为了简化演示,所以使用了GET请求

自定义参数解析器HandlerMethodArgumentResolver最重要不是它本身的实现,而是它的指导思想:分离关注,业务解耦。当然本文我摘出来的两个使用场景案例只是冰山一角,各位需要举一反三。

既然我们可以自定义参数处理器HandlerMethodArgumentResolver,自然也就可以自定义返回值处理器HandlerMethodReturnValueHandler喽。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 585015浏览

随机文章
Java—并发编程(七)JUC集合 – (9) LinkedBlockingDeque
3年前
Docker—数据卷(四)
5年前
Spring笔记9—Aop
5年前
Spring笔记15—声明式事务
5年前
Spring笔记7—条件注解
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1909 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 585015 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付