参考/原地址:HandlerMethodArgumentResolver(二):Map参数类型和固定参数类型【享学Spring MVC】 - 云+社区 - 腾讯云 (tencent.com)
前面介绍了Spring MVC
用于处理入参的处理器:HandlerMethodReturnValueHandler
它的作用,以及介绍了最为常用的两个参数处理器子类:PathVariableMethodArgumentResolver
和RequestParamMethodArgumentResolver。
第一类:基于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; }
MatrixVariableMethodArgumentResolver
标注有@MatrixVariable
注解的参数的处理器。Matrix:矩阵
,这个注解是Spring3.2新提出来的,增强Restful的处理能力(配合@PathVariable
使用),比如这类URL的解析就得靠它:/owners/42;q=11/pets/21;s=23;q=22
。
关于@MatrixVariable
它的使用案例,我找了两篇靠谱文章给你参考: 参考一 参考二
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建议还是只使用在业务层
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
/占位符获取不到值了,其实这是**不正确
**的。理由如下:(可略过,SpringBoot是单个容器,不存在父子容器)
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
的
这类解析器我认为是对第一类的有些处理器的一种补充,它依赖上面的相关注解。 你是否想过通过@RequestParam
一次性全给封装进一个Map
里,然后再自己分析?同样的本类处理器给@RequestHeader
、@PathVariable
、@MatrixVariable
都赋予了这种能力
PathVariableMapMethodArgumentResolver
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
略
MapMethodProcessor
它处理Map类型,但没有标注任何注解的情况,它的执行顺序是很靠后的,所以有点兜底的意思。
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); } }
ServletResponseMethodArgumentResolver
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
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
相关。
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
一个特殊的处理器:ModelAttributeMethodProcessor
:主要是针对 被 @ModelAttribute
注解修饰且不是普通类型(通过 !BeanUtils.isSimpleProperty
来判断)的参数。
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
这块的使用,参见这里