HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学Spring MVC】

本文主要是介绍HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学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用于数据绑定、校验@NullableObject 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 {@Nullableprivate final ConfigurableBeanFactory configurableBeanFactory;@Nullableprivate 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;// 默认是RequestScopethis.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null);}// protected的内部类  所以所有子类(注解)都是用友这三个属性值的protected static class NamedValueInfo {private final String name;private final boolean required;@Nullableprivate final String defaultValue;public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {this.name = name;this.required = required;this.defaultValue = defaultValue;}}// 核心方法  注意此方法是final的,并不希望子类覆盖掉他~@Override@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {// 创建 MethodParameter 对应的 NamedValueInfoNamedValueInfo namedValueInfo = getNamedValueInfo(parameter);// 支持到了Java 8 中支持的 java.util.OptionalMethodParameter 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类型,@Overridepublic 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;}@Overrideprotected 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@Nullableprotected 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的子类@Overrideprotected 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的使用,不用再给例子了。

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

对此文答案有兴趣的可参见此文:SpringMVC RESTful 性能优化


唯一需要说一下如果类型是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等~// 默认值是falseprivate final boolean useDefaultResolution;// 此构造只有`MvcUriComponentsBuilder`调用了  传入的falsepublic 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情况下,"基本类型"也会处理@Overridepublic 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来的@Overrideprotected 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@Nullableprotected 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 resolutionresolvers.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】


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

这篇关于HandlerMethodArgumentResolver(一):Controller方法入参自动封装器(将参数parameter解析为值)【享学Spring MVC】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/351814

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G