GVKun编程网logo

使用自定义HandlerMethodArgumentResolver的Spring MVC @Valid验证(spring自定义校验)

32

对于使用自定义HandlerMethodArgumentResolver的SpringMVC@Valid验证感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍spring自定义校验,并为您提供关于

对于使用自定义HandlerMethodArgumentResolver的Spring MVC @Valid验证感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍spring自定义校验,并为您提供关于1. 盘点 springmvc 的常用接口之 HandlerMethodArgumentResolver、HandlerMethodArgumentResolver、HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器【享学 Spring MVC】、HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】的有用信息。

本文目录一览:

使用自定义HandlerMethodArgumentResolver的Spring MVC @Valid验证(spring自定义校验)

使用自定义HandlerMethodArgumentResolver的Spring MVC @Valid验证(spring自定义校验)

我想注册一个HandlerMethodArgumentResolver可以处理以下@Controller处理程序方法定义的自定义

@RequestMapping(method = RequestMethod.POST)public String createDomain(@Valid Domain domain, BindingResult errors, @RequestParam("countryId") Long countryId) {

我可以注册我的解析器,这只是创建了一个Domain通过重写通过请求参数对象,addArgumentResolver()WebMvcConfigurerAdapter。当Spring尝试解析该Domain参数时,它将遍历其列表HandlerMethodArgumentResolver(有很多),并选择supports()它的第一个。

在上面的例子中,虽然我的解析器将被调用,我的Domain论点将得到初始化,@Valid注释将不会被处理 ,并
为解析器BindingResult,一个ErrorsMethodArgumentResolver是因为它需要一个会失败@ModelAttribute@RequestBody@RequestPart在处理方法的说法,这我不没有。

如果我尝试通过添加来修复它 @ModelAttribute

@RequestMapping(method = RequestMethod.POST)public String createDomain(@Valid @ModelAttribute Domain domain, BindingResult errors, @RequestParam("countryId") Long countryId) {

HandlerMethodArgumentResolver实施ModelAttributeMethodProcessor,将首先与检查supports()并解决的说法(与@ModelAttribute@Valid我的自定义冲突解决之前)。该BindingResult不会失败,但我不会对我的自定义创建行为Domain的实例。

我可以复制粘贴代码以进行验证并将其添加到中的模型中ModelAttributeMethodProcessor,但是我希望有一种更简单的方法来解析参数并执行验证,而无需在模型中添加对象。有这种方法吗?

答案1

小编典典

很好地描述了您所面对的问题。

我检查了您概述的代码,并得出了相同的结论-没有内置的方法可以同时应用自定义
HandlerMethodArgumentResolver@Valid相关验证,唯一的选择就是执行所述ModelAttributeMethodProcessor确实这是检查该参数具有@Valid注释和调用验证逻辑相关的代码。

您可能可以从中派生您的代码HandlerMethodResolverArgumentResolverModelAttributeMethodProcessorsuper.validateIfApplicable(..)至少以这种方式调用现有代码。

1. 盘点 springmvc 的常用接口之 HandlerMethodArgumentResolver

1. 盘点 springmvc 的常用接口之 HandlerMethodArgumentResolver

1. 盘点 springmvc 的常用接口之 HandlerMethodArgumentResolver###

前言

在初学 springmvc 框架时,我就一直有一个疑问,为什么 controller 方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如 HttpServletRequest 或 HttpServletResponse,各种注解 @RequestParam@RequestHeader@RequestBody@PathVariable@ModelAttribute 等。相信很多初学者都曾经感慨过。

这一章就是讲解处理这方面工作的

org.springframework.web.method.support.HandlerMethodArgumentResolver 接口。

springmvc 自带的一些实现:

  • ServletRequestMethodArgumentResolverServletResponseMethodArgumentResolver 处理了自动绑定 HttpServletRequest 和 HttpServletResponse
  • RequestParamMapMethodArgumentResolver 处理了 @RequestParam
  • RequestHeaderMapMethodArgumentResolver 处理了 @RequestHeader
  • PathVariableMapMethodArgumentResolver 处理了 @PathVariable
  • ModelAttributeMethodProcessor 处理了 @ModelAttribute
  • RequestResponseBodyMethodProcessor 处理了 @RequestBody
  • ……

我们可以模仿 springmvc 的源码,实现一些我们自己的实现类,而方便我们的代码开发。

接口说明

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodArgumentResolver {
	//用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
	boolean supportsParameter(MethodParameter parameter);
	//真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。
	Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}

示例 1

本示例显示如何 优雅地将传入的信息转化成自定义的实体传入 controller 方法。

实现访问 POST http://localhost:8080/demo1

post 数据

first_name = Bill

last_name = Gates

初学者一般喜欢类似下面的代码:

package com.demo.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.demo.domain.Person;
import com.demo.mvc.annotation.MultiPerson;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("demo1")
public class HandlerMethodArgumentResolverDemoController {

  	@ResponseBody
	@RequestMapping(method = RequestMethod.POST)
	public String addPerson(HttpServletRequest request) {
		String firstName = request.getParameter("first_name");
		String lastName = request.getParameter("last_name");
		Person person = new Person(firstName, lastName);
		log.info(person.toString());
		return person.toString();
	}
}

这样的代码强依赖了 javax.servlet-api 的 HttpServletRequest 对象,并且把初始化 Person 对象这 “活儿” 加塞给了 controller。代码显得累赘不优雅。在 controller 里我只想使用 person 而不想组装 person,想要类似下面的代码:

@RequestMapping(method = RequestMethod.POST)
public String addPerson(Person person) {
  log.info(person.toString());
  return person.toString();
}

直接在形参列表中获得 person。那么这该如实现呢?

我们需要定义如下的一个参数分解器:

package com.demo.mvc.component;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.demo.domain.Person;

public class PersonArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.getParameterType().equals(Person.class);
	}

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

		String firstName = webRequest.getParameter("first_name");
		String lastName = webRequest.getParameter("last_name");
		return new Person(firstName, lastName);
	}

}

supportsParameter 中判断是否需要启用分解功能,这里判断形参类型是否为 Person 类,也就是说当形参遇到 Person 类时始终会执行该分解流程 resolveArgument

resolveArgument 中处理 person 的初始化工作。

注册自定义分解器:

spring-boot 方式

package com.demo;

import java.util.List;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import com.demo.mvc.component.MultiPersonArgumentResolver;
import com.demo.mvc.component.PersonArgumentResolver;

@SpringBootApplication
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

	@Override
	protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {

		// 注册Person的参数分解器
		argumentResolvers.add(new PersonArgumentResolver());
	}
}

或者传统 XML 配置:

<mvc:annotation-driven>
  	<mvc:argument-resolvers>
    	<bean class="com.demo.mvc.component.PersonArgumentResolver"/>
  	</mvc:argument-resolvers>
</mvc:annotation-driven>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
	<property name="customArgumentResolvers">
      	<bean class="com.demo.mvc.component.PersonArgumentResolver"/>
    </property>
</bean>

#### 示例 2

加强版 Person 分解器,支持多个 person 对象。

比如 POST http://localhost:8080/demo1

post 数据

person1.first_name = Bill

person1.last_name = Gates

person2.first_name = Steve

person2.last_name = Jobs

用前缀区分属于哪个 person 对象。

定义一个注解用于设定前缀:

package com.demo.mvc.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiPerson {

	public String value();
}

参数分解器:

package com.demo.mvc.component;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.demo.domain.Person;
import com.demo.mvc.annotation.MultiPerson;

public class MultiPersonArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(MultiPerson.class) && parameter.getParameterType().equals(Person.class);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		MultiPerson annotation = parameter.getParameterAnnotation(MultiPerson.class);
		String firstName = webRequest.getParameter(annotation.value() + ".first_name");
		String lastName = webRequest.getParameter(annotation.value() + ".last_name");
		return new Person(firstName, lastName);
	}

}

controller:

@ResponseBody
@RequestMapping(value = "multi", method = RequestMethod.POST)
public String addPerson(@MultiPerson("person1") Person person1, @MultiPerson("person2") Person person2) {
  log.info(person1.toString());
  log.info(person2.toString());
  return person1.toString() + "\n" + person2.toString();
}

友情链接:

盘点 springmvc 的常用接口目录

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver

Spring的参数解析器顶层接口,将 HttpServletRequest(header + body 中的内容)解析为对应参数类型的对象,其中定义了2个方法

public interface HandlerMethodArgumentResolver {

    //是否支持解析该参数
    boolean supportsParameter(MethodParameter parameter);

    //解析参数
    Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}

Spring启动时默认加载的HandlerMethodArgumentResolver实现类

  • RequestAttributeMethodArgumentResolver
  • SessionAttributeMethodArgumentResolver
  • ServletRequestMethodArgumentResolver
  • ServletResponseMethodArgumentResolver
  • RedirectAttributesMethodArgumentResolver
  • ModelMethodProcessor

HandlerMethodArgumentResolver 的设计模式

  • 策略模式:顶层 HandlerMethodArgumentResolver定义解析参数的方法,根据不同得策略实现对应的子类
  • 责任链模式:HandlerMethodArgumentResolverComposite.

argumentResolvers循环遍历解析器,能解析则直接解析,不能则向下传递

  • 模版模式:AbstractMessageConverterMethodArgumentResolver中定义解析参数的主逻辑, 子类 HttpEntityMethodProcessor | RequestResponseBodyMethodProcessor实现具体的逻辑

HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器【享学 Spring MVC】

HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器【享学 Spring MVC】

每篇一句

你的工作效率高,老板会认为你强度不够。你代码 bug 多,各种生产环境救火,老板会觉得你是团队的核心成员。

前言

在享受 Spring MVC 带给你便捷的时候,你是否曾经这样疑问过:Controllerhandler 方法参数能够自动完成封装(有时即使没有 @PathVariable@RequestParam@RequestBody 等注解都可),甚至在方法参数任意位置写 HttpServletRequestHttpSessionWriter... 等类型的参数,它自动就有值了便可直接使用。 对此你是否想问一句:Spring MVC 它是怎么办到的?那么本文就揭开它的神秘面纱,还你一片 "清白"。

Spring MVC 作为一个最为流行的 web 框架,早早已经成为了实际意义上的标准化(框架),特别是随着 Struts2 的突然崩塌,Spring MVC 几乎一骑绝尘,因此深入了解它有着深远的意义

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

像 JSF、Google Web Toolkit、Grails Framework 等 web 框架至少我是没有用过的。 这里有个轻量级的 web 框架:Play Framework 设计上我个人觉得还挺有意思,有兴趣的可以玩玩

HandlerMethodArgumentResolver

策略接口:用于在给定请求的上下文中将方法参数解析为参数值。简单的理解为:它负责处理你 Handler 方法里的所有入参:包括自动封装、自动赋值、校验等等。有了它才能会让 Spring MVC 处理入参显得那么高级、那么自动化。 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. 通过模版方法 resolveNameHttpServletRequest, Http Headers, URI template variables 等等中获取对应的属性值(具体由子类去实现)
  4. arg==null 这种情况的处理,要么使用默认值,若 required = true && arg == null, 则一般报出异常(boolean 类型除外~)
  5. 通过 WebDataBinderarg 转换成 Methodparameter.getParameterType() 类型(注意:这里仅仅只是用了数据转换而已,并没有用 bind() 方法)

该抽象类继承树如下: 在这里插入图片描述 从上源码可以看出,抽象类已经定死了处理模版(方法为 final 的),留给子类需要做的事就不多了,大体还有如下三件事:

  1. 根据 MethodParameter 创建 NameValueInfo(子类的实现可继承自 NameValueInfo,就是对应注解的属性们)
  2. 根据方法参数名称 nameHttpServletRequest, 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);
	}
	...
}

关于 @PathVariable 的使用,不用再给例子了。 唯一需要说一下如果类型是 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());
	}


	// 核心方法:根据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;
	}
	...
}

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




在本文末,我搜集了一些自己使用过程中的一些疑惑进行解惑,希望也一样能帮助你豁然开朗。

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

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

@PathVariable 传参
    @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 也是可以正常接收的)。 使用时应注意如下两点:

  1. 多个值只能使用 , 号分隔才行(否则会被当作一个值,放进数组 / 集合里,不会报错)
  2. @PathVariable 注解是必须的。否则会交给 ServletModelAttributeMethodProcessor 兜底去处理,它要求有空构造所以反射创建实例会报错(数组 / List)。(注意:如果是这样写 ArrayList<Object> 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 这么写的话不写注解是不行的(报错如上)。至于原因,各位小伙伴可以自行思考,没想明白的话可以给我留言(建议小伙伴一定要弄明白缘由)~


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

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

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


总结

本文是你理解 Spring MVC 强大的自动数据封装功能非常重要的一篇文章。它介绍了 HandlerMethodArgumentResolver 的功能和基本使用,以及深入介绍了最为重要的两个注解 @PathVariable@RequestParam 以及各自对应的 ArgumentResolver 处理器。 由于这个体系庞大,所以我会分多个章节进行描述,欢迎订阅和持续关注~

相关阅读

【小家 Spring】Spring MVC 容器的 web 九大组件之 ---HandlerMapping 源码详解 (二)---RequestMappingHandlerMapping 系列

HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器(将参数 parameter 解析为值)【享学 Spring MVC】 HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】 HandlerMethodArgumentResolver (三):基于 HttpMessageConverter 消息转换器的参数处理器【享学 Spring MVC】

知识交流

==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~==

若对技术内容感兴趣可以加入 wx 群交流:Java高工、架构师3群。 若群二维码失效,请加 wx 号:fsx641385712(或者扫描下方 wx 二维码)。并且备注:"java入群" 字样,会手动邀请入群 == 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==

HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】

HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】

每篇一句

黄金的导电性最好,为什么电脑主板还是要用铜? 飞机最快,为什么还有人做火车? 清华大学最好,为什么还有人去普通学校? 因为资源都是有限的,我们现实生活中必须兼顾成本与产出的平衡

前言

上文 介绍了 Spring MVC 用于处理入参的处理器:HandlerMethodReturnValueHandler 它的作用,以及介绍了最为常用的两个参数处理器子类:PathVariableMethodArgumentResolverRequestParamMethodArgumentResolver。由于该体系的重要以及庞大,本文将接着继续讲解~

第一类:基于 Name(续)

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]

Tip:注解指定的 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

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

MatrixVariableMethodArgumentResolver

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

关于 @MatrixVariable 它的使用案例,我找了两篇靠谱文章给你参考: 参考一 参考二

// @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;
	}
	...
}

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

略。

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.ModelRedirectAttributesRedirectAttributesModelMapConcurrentModelExtendedModelMap 等等


在本文末尾,说一个特殊的处理器: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())));
	}
}

关于 @ModelAttribute 这块的使用,参见这里

总结

本文介绍完了四大类的前面三种类型,其中最为常用的是前两种类型的使用,希望大家可以掌握,和好好发挥~

相关阅读

HandlerMethodArgumentResolver:Controller 入参自动封装器(将方法参数 parameter 解析为参数值)【享学 Spring MVC】 从原理层面掌握 @ModelAttribute 的使用(核心原理篇)【享学 Spring MVC】 从原理层面掌握 @ModelAttribute 的使用(使用篇)【享学 Spring MVC】

HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器(将参数 parameter 解析为值)【享学 Spring MVC】 HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】 HandlerMethodArgumentResolver (三):基于 HttpMessageConverter 消息转换器的参数处理器【享学 Spring MVC】

知识交流

==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~==

若对技术内容感兴趣可以加入 wx 群交流:Java高工、架构师3群。 若群二维码失效,请加 wx 号:fsx641385712(或者扫描下方 wx 二维码)。并且备注:"java入群" 字样,会手动邀请入群 == 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==

原文出处:https://www.cnblogs.com/fangshixiang/p/11423602.html

今天关于使用自定义HandlerMethodArgumentResolver的Spring MVC @Valid验证spring自定义校验的介绍到此结束,谢谢您的阅读,有关1. 盘点 springmvc 的常用接口之 HandlerMethodArgumentResolver、HandlerMethodArgumentResolver、HandlerMethodArgumentResolver (一):Controller 方法入参自动封装器【享学 Spring MVC】、HandlerMethodArgumentResolver (二):Map 参数类型和固定参数类型【享学 Spring MVC】等更多相关知识的信息可以在本站进行查询。

本文标签: