阅读完需:约 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;
}
基于这个接口的处理器实现类不可谓不丰富,非常之多。我截图如下:

可以分为四类进行描述
- 基于
Name
- 数据类型是
Map
的 - 固定参数类型
- 基于
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;
}
该抽象类中定义了解析参数的主逻辑(模版逻辑),子类只需要实现对应的抽象模版方法即可。
对此部分的处理步骤,我把它简述如下:
- 基于
MethodParameter
构建NameValueInfo
<– 主要有name, defaultValue, required
(其实主要是解析方法参数上标注的注解~) - 通过
BeanExpressionResolver
(${}占位符以及SpEL) 解析name - 通过模版方法
resolveName
从HttpServletRequest
,Http Headers, URI template variables
等等中获取对应的属性值(具体由子类去实现) - 对 arg==null这种情况的处理, 要么使用默认值, 若 required = true && arg == null, 则一般报出异常(boolean类型除外~)
- 通过
WebDataBinder
将arg转换成Methodparameter.getParameterType()
类型(注意:这里仅仅只是用了数据转换而已,并没有用bind()方法)
该抽象类继承树如下:

从上源码可以看出,抽象类已经定死了处理模版(方法为final的),留给子类需要做的事就不多了,大体还有如下三件事:
- 根据
MethodParameter
创建NameValueInfo
(子类的实现可继承自NameValueInfo
,就是对应注解的属性们) - 根据方法参数名称name从
HttpServletRequest, Http Headers, URI template variables
等等中获取属性值 - 对
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}
从结果看出:
- 它不能传一key多值情况
- 若出现相同的key,以在最前面的key的值为准。
- 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
它深层的作用原理,那么本文就通过自定义参数处理器,来做到屏蔽(隔离)基础实现、更高效的编写业务编码。
关于它的应用场景可以非常多,本文我总结出最为常见、好理解的两个应用场景作为举例说明:
- 获取当前登陆人(当然用户)的基本信息
- 调整(兼容)数据结构
场景一:
在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
老系统),你不乏会遇到如下两个痛点:
- 对方系统是以下划线形式命名的(和Java命名规范相悖)
- 对方系统的参数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
全部接收回来(字段命名也用下划线方式命名),然后再一个个处理。
有如下两个不妥的地方:
- Java属性名也必须用下划线命名,看起来影响了命名体系(其实就是看着不爽,哈哈)
- 按照参数这种复杂结构书写,使得我们关注点分散,不能聚焦到真真关心的那一块数据上
针对这些痛点,废话不多说,直接上处理方案:
定义一个模型(只写我自己关注的属性)
@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
喽。