每篇一句
黄金的导电性最好,为什么电脑主板还是要用铜?
飞机最快,为什么还有人做火车?
清华大学最好,为什么还有人去普通学校?
因为资源都是有限的,我们现实生活中必须兼顾成本与产出的平衡
前言
上文 介绍了 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;
}
手动设置一个 cookie 值,然后请求
控制台打印如下:
javax.servlet.http.Cookie@401ef395
123456
Tips:在现在 restful 风格下,cookie 使用得是很少的了。一般用于提升用户体验方面~
MatrixVariableMethodArgumentResolver
标注有 @MatrixVariable
注解的参数的处理器。Matrix:矩阵
,这个注解是 Spring3.2 新提出来的,增强 Restful 的处理能力(配合@PathVariable
使用),比如这类 URL 的解析就得靠它:/owners/42;q=11/pets/21;s=23;q=22
。
关于 @MatrixVariable
它的使用案例,我找了两篇靠谱文章给你参考:
参考一
参考二
// @since 3.2
public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
// @MatrixVariable 注解是必须的。然后技能处理普通类型,也能处理 Map
@Override
public boolean supportsParameter(MethodParameter parameter) {if (!parameter.hasParameterAnnotation(MatrixVariable.class)) {return false;}
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class);
return (matrixVariable != null && StringUtils.hasText(matrixVariable.name()));
}
return true;
}
...
}
ExpressionValueMethodArgumentResolver
它用于处理标注有 @Value
注解的参数。对于这个注解我们太熟悉不过了,没想到在 web 层依旧能发挥作用。本文就重点来会会它~
通过 @Value
让我们在配置文件里给参数赋值,在某些特殊场合(比如前端不用传,但你想给个默认值,这个时候用它也是一种方案)
说明:这就相当于在 Controller 层使用了 @Value 注解,其实我是不太建议的。因为 @Value 建议还是只使用在业务层~
// @since 3.1
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
// 唯一构造函数 支持占位符、SpEL
public ExpressionValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {super(beanFactory);
}
// 必须标注有 @Value 注解
@Override
public boolean supportsParameter(MethodParameter parameter) {return parameter.hasParameterAnnotation(Value.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {Value ann = parameter.getParameterAnnotation(Value.class);
return new ExpressionValueNamedValueInfo(ann);
}
private static final class ExpressionValueNamedValueInfo extends NamedValueInfo {
// 这里 name 传值为固定值 因为只要你的 key 不是这个就木有问题
// required 传固定值 false
// defaultValue:取值为 annotation.value() --> 它天然支持占位符和 SpEL 嘛
private ExpressionValueNamedValueInfo(Value annotation) {super("@Value", false, annotation.value());
}
}
// 这里恒返回 null,因此即使你的 key 是 @Value,也是不会采纳你的传值的哟~
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
// No name to resolve
return null;
}
}
根本原理其实只是利用了 defaultValue
支持占位符和 SpEL
的特性而已。给个使用示例:
// 在 MVC 子容器中导入外部化配置
@Configuration
@PropertySource("classpath:my.properties") // 此处有键值对:test.myage = 18
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {...}
@ResponseBody
@GetMapping("/test")
public Object test(@Value("#{T(Integer).parseInt('${test.myage:10}') + 10}") Integer myAge) {System.out.println(myAge);
return myAge;
}
请求:/test
,打印:28
。
注意:若你写成@Value("#{'${test.myage:10}' + 10}
,那你得到的答案是:1810
(成字符串拼接了)。
另外,我看到网上有不少人说如果把这个 @PropertySource("classpath:my.properties")
放在根容器的 config 文件里导入,controller 层就使用 @Value
/ 占位符获取不到值了,其实这是 不正确
的。理由如下:
Spring MVC
子容器在创建时:initWebApplicationContext()
if (cwac.getParent() == null) {cwac.setParent(rootContext); // 设置上父容器(根容器)}
AbstractApplicationContext:如下代码
// 相当于子容器的环境会把父容器的 Enviroment 合并进来
@Override
public void setParent(@Nullable ApplicationContext parent) {
this.parent = parent;
if (parent != null) {Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}
AbstractEnvironment:merge()方法如下
@Override
public void merge(ConfigurableEnvironment parent) {
// 完全的从 parent 里所有的 PropertySources 里拷贝一份进来
for (PropertySource<?> ps : parent.getPropertySources()) {if (!this.propertySources.contains(ps.getName())) {this.propertySources.addLast(ps);
}
}
...
}
这就是为什么说即使你是在根容器里使用的 @PropertySource
导入的外部资源,子容器也可以使用的原因(因为子容器会把父环境给 merge
一份过来)。
但是,但是,但是:如果你是使用形如
PropertyPlaceholderConfigurer
这种方式导进来的,那是会有容器隔离效应的~
第二类:参数类型是 Map
的
数据来源同上,只是参数类型是 Map
这类解析器我认为是对第一类的有些处理器的一种补充,它依赖上面的相关注解。
你是否想过通过 @RequestParam
一次性全给封装进一个 Map
里,然后再自己分析?同样的本类处理器给 @RequestHeader
、@PathVariable
、@MatrixVariable
都赋予了这种能力~
PathVariableMapMethodArgumentResolver
// @since 3.2 晚一个版本号
public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
// 必须标注 @PathVariable 注解 并且类型是 Map,并且注解不能有 value 值
// 处理情况和 PathVariableMethodArgumentResolver 形成了互补
@Override
public boolean supportsParameter(MethodParameter parameter) {PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(ann.value()));
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {... // 处理上极其简单,把所有的路径参数使用 Map 装着返回即可}
}
RequestParamMapMethodArgumentResolver
它依赖的方法是:HttpServletRequest#getParameterMap()
、MultipartRequest#getMultiFileMap()
、MultipartRequest#getFileMap()
等,出现于Spring 3.1
。
演示一把:
@ResponseBody
@GetMapping("/test")
public Object test(@RequestParam Map<String,Object> params) {System.out.println(params);
return params;
}
请求:/test?name=fsx&age=18&age=28
。打印
{name=fsx, age=18}
从结果看出:
- 它不能传一 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 类型,但 没有标注任何注解的情况,它的执行顺序是很靠后的,所以有点兜底的意思。
// @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())));
}
}
关于 @ModelAttribute
这块的使用,参见这里
总结
本文介绍完了四大类的前面三种类型,其中最为常用的是前两种类型的使用,希望大家可以掌握,和好好发挥~
相关阅读
HandlerMethodArgumentResolver:Controller 入参自动封装器(将方法参数 parameter 解析为参数值)【享学 Spring MVC】
从原理层面掌握 @ModelAttribute 的使用(核心原理篇)【享学 Spring MVC】
从原理层面掌握 @ModelAttribute 的使用(使用篇)【享学 Spring MVC】
HandlerMethodArgumentResolver(一):Controller 方法入参自动封装器(将参数 parameter 解析为值)【享学 Spring MVC】
HandlerMethodArgumentResolver(二):Map 参数类型和固定参数类型【享学 Spring MVC】
HandlerMethodArgumentResolver(三):基于 HttpMessageConverter 消息转换器的参数处理器【享学 Spring MVC】
知识交流
==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被 作者本人许可的~
==
** 若对技术内容感兴趣可以加入 wx 群交流:Java 高工、架构师 3 群
。
若群二维码失效,请加 wx 号:fsx641385712
(或者扫描下方 wx 二维码)。并且备注:"java 入群"
字样,会手动邀请入群 **
== 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==