阅读完需:约 14 分钟
在一个 Web 请求中,参数我们无非就是放在地址栏或者请求体中,个别请求可能放在请求头中。
放在地址栏中,我们可以通过如下方式获取参数:
String name = request.getParameter("name ");
放在请求体中,如果是 key/value 形式,我们可以通过如下方式获取参数:
String name = request.getParameter("name ");
如果是 JSON 形式,我们则通过如果如下方式获取到输入流,然后解析成 JSON 字符串,再通过 JSON 工具转为对象:
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String json = reader.readLine();
reader.close();
User user = new ObjectMapper().readValue(json, User.class);
如果参数放在请求头中,我们可以通过如下方式获取:
如果用的是 Jsp/Servlet 那一套技术栈,那么参数获取无外乎这几种方式。
如果用了 SpringMVC 框架,那参数获取方式太丰富了,各种注解如 @RequestParam
、@RequestBody
、@RequestHeader
、@PathVariable
,参数可以是 key/value 形式,也可以是 JSON 形式,非常丰富!但是,无论多么丰富,最底层获取参数的方式无外乎上面几种。
参数提取
SpringMVC 到底是怎么样从 request 中把参数提取出来?
例如下面这个接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(String name) {
return "hello "+name;
}
}
我们都知道 name 参数是从 HttpServletRequest
中提取出来的,到底是怎么提取出来的?
自定义参数解析器
SpringBoot依赖版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
之前在探究MVC接口请求的时候就有过发现,自定义参数解析器需要实现 HandlerMethodArgumentResolver
接口
从上面的文章上可以得到SpringMVC是如何使用这个HandlerMethodArgumentResolver
的
HandlerMethodArgumentResolver
的接口探究
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
![](http://www.enmalvi.com/wp-content/uploads/2022/09/image-76.png)
这个接口中就两个方法:
-
supportsParameter
:该方法表示是否启用这个参数解析器,返回 true 表示启用,返回 false 表示不启用。 -
resolveArgument
:这是具体的解析过程,就是从 request 中取出参数的过程,方法的返回值就对应了接口中参数的值。
自定义参数解析器只需要实现该接口即可。
案例
给接口上的参数赋值,原本是从HttpServletRequest
中获取,但是测试为了方便直接手动赋值
可以先写一个接口做测试接口
@RestController
public class HelloController {
/**
* supportsParameter:如果参数类型是 String,并且参数上有 @CurrentUserName 注解,则使用该参数解析器。
*
* @param name
* @return
*/
@GetMapping("/user")
public String hello(@CurrentUserName String name) {
return "hello "+name;
}
}
其次需要一个自定义注解来表明需要赋值的对象
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CurrentUserName {
}
重头戏是我们自定义参数解析器 CurrentUserNameHandlerMethodArgumentResolver
,如下:
/**
* 自定义参数解析
* @author xujiahui
*/
public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* supportsParameter:该方法表示是否启用这个参数解析器,返回 true 表示启用,返回 false 表示不启用。
* @param parameter the method parameter to check
* @return
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(String.class)&¶meter.hasParameterAnnotation(CurrentUserName.class);
}
/**
* resolveArgument:这是具体的解析过程,就是从 request 中取出参数的过程,方法的返回值就对应了接口中参数的值。
*
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return
* @throws Exception
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
User user=new User();
user.setName("测试数据");
return user.getName();
}
}
最后,我们再将自定义的参数解析器配置到 HandlerAdapter
中,配置方式如下:
/**
* 最后将自定义的参数解析器配置到 HandlerAdapter 中
* @author xujiahui
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserNameHandlerMethodArgumentResolver());
}
}
至此,就算配置完成了。
接下来启动项目,访问 /hello
接口,就可以看到返回当前登录用户数据了。
这就是我们自定义的一个参数类型解析器。可以看到,非常 Easy。
在 SpringMVC 中,默认也有很多 HandlerMethodArgumentResolver
的实现类。
参数解析器
HandlerMethodArgumentResolver
就是我们口口声声说的参数解析器,它的实现类还是蛮多的,因为每一种类型的参数都对应了一个参数解析器:
为了理解方便,我们可以将这些参数解析器分为四大类:
-
xxxMethodArgumentResolver
:这就是一个普通的参数解析器。 -
xxxMethodProcessor
:不仅可以当作参数解析器,还可以处理对应类型的返回值。 -
xxxAdapter
:这种不做参数解析,仅仅用来作为WebArgumentResolver
类型的参数解析器的适配器。 -
HandlerMethodArgumentResolverComposite
:这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。
大致上可以分为这四类,其中最重要的当然就是前两种了。
参数解析器概览
接下来我们来先来大概看看这些参数解析器分别都是用来干什么的。
MapMethodProcessor
这个用来处理 Map/ModelMap 类型的参数,解析完成后返回 model。
PathVariableMethodArgumentResolver
这个用来处理使用了 @PathVariable
注解并且参数类型不为 Map 的参数,参数类型为 Map 则使用 PathVariableMapMethodArgumentResolver
来处理。
PathVariableMapMethodArgumentResolver
见上。
ErrorsMethodArgumentResolver
这个用来处理 Error 参数,例如我们做参数校验时的 BindingResult。
AbstractNamedValueMethodArgumentResolver
这个用来处理 key/value 类型的参数,如请求头参数、使用了 @PathVariable
注解的参数以及 Cookie 等。
RequestHeaderMethodArgumentResolver
这个用来处理使用了 @RequestHeader
注解,并且参数类型不是 Map 的参数(参数类型是 Map 的使用 RequestHeaderMapMethodArgumentResolver
)。
RequestHeaderMapMethodArgumentResolver
见上。
RequestAttributeMethodArgumentResolver
这个用来处理使用了 @RequestAttribute
注解的参数。
RequestParamMethodArgumentResolver
这个功能就比较广了。使用了 @RequestParam
注解的参数、文件上传的类型 MultipartFile、或者一些没有使用任何注解的基本类型(Long、Integer)以及 String 等,都使用该参数解析器处理。需要注意的是,如果 @RequestParam
注解的参数类型是 Map,则该注解必须有 name 值,否则解析将由 RequestParamMapMethodArgumentResolver
完成。
RequestParamMapMethodArgumentResolver
见上。
AbstractCookieValueMethodArgumentResolver
这个是一个父类,处理使用了 @CookieValue
注解的参数。
ServletCookieValueMethodArgumentResolver
这个处理使用了 @CookieValue
注解的参数。
MatrixVariableMethodArgumentResolver
这个处理使用了 @MatrixVariable
注解并且参数类型不是 Map 的参数,如果参数类型是 Map,则使用 MatrixVariableMapMethodArgumentResolver
来处理。
MatrixVariableMapMethodArgumentResolver
见上。
SessionAttributeMethodArgumentResolver
这个用来处理使用了 @SessionAttribute
注解的参数。
ExpressionValueMethodArgumentResolver
这个用来处理使用了 @Value
注解的参数。
ServletResponseMethodArgumentResolver
这个用来处理 ServletResponse、OutputStream 以及 Writer 类型的参数。
ModelMethodProcessor
这个用来处理 Model 类型参数,并返回 model。
ModelAttributeMethodProcessor
这个用来处理使用了 @ModelAttribute
注解的参数。
SessionStatusMethodArgumentResolver
这个用来处理 SessionStatus 类型的参数。
PrincipalMethodArgumentResolver
这个用来处理 Principal 类型参数,这是SpringSecurity框架中用来获取用户名的处理器
AbstractMessageConverterMethodArgumentResolver
这是一个父类,当使用 HttpMessageConverter 解析 requestbody 类型参数时,相关的处理类都会继承自它。
RequestPartMethodArgumentResolver
这个用来处理使用了 @RequestPart
注解、MultipartFile 以及 Part 类型的参数。
AbstractMessageConverterMethodProcessor
这是一个工具类,不承担参数解析任务。
RequestResponseBodyMethodProcessor
这个用来处理添加了 @RequestBody
注解的参数。
HttpEntityMethodProcessor
这个用来处理 HttpEntity 和 RequestEntity 类型的参数。
ContinuationHandlerMethodArgumentResolver
AbstractWebArgumentResolverAdapter
这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。
ServletWebArgumentResolverAdapter
这个给父类提供 request。
UriComponentsBuilderMethodArgumentResolver
这个用来处理 UriComponentsBuilder 类型的参数。
ServletRequestMethodArgumentResolver
这个用来处理 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 类型的参数。
HandlerMethodArgumentResolverComposite
这个看名字就知道是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。
RedirectAttributesMethodArgumentResolver
这个用来处理 RedirectAttributes 类型的参数。
各个参数解析器的大致功能就给大家介绍完了,接下来我们选择其中一种,来具体说说它的源码。
AbstractNamedValueMethodArgumentResolver
AbstractNamedValueMethodArgumentResolver
是一个抽象类,一些键值对类型的参数解析器都是通过继承它实现的,它里边定义了很多这些键值对类型参数解析器的公共操作。
![](http://www.enmalvi.com/wp-content/uploads/2022/09/image-77.png)
AbstractNamedValueMethodArgumentResolver
中也是应用了很多模版模式,例如它没有实现 supportsParameter
方法,该方法的具体实现在不同的子类中,resolveArgument
方法它倒是实现了,我们一起来看下
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
// Check for null value after conversion of incoming argument value
if (arg == null && namedValueInfo.defaultValue == null &&
namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
}
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
- 1. 首先根据当前请求获取一个
NamedValueInfo
对象,这个对象中保存了参数的三个属性:参数名、参数是否必须以及参数默认值。具体的获取过程就是先去缓存中拿,缓存中如果有,就直接返回,缓存中如果没有,则调用createNamedValueInfo
方法去创建,将创建结果缓存起来并返回。createNamedValueInfo
方法是一个模版方法,具体的实现在子类中。 - 2. 接下来处理
Optional
类型参数。 - 3.
resolveEmbeddedValuesAndExpressions
方法是为了处理注解中使用了 SpEL 表达式的情况,例如如下接口:
@GetMapping("/hello2")
public void hello2(@RequestParam(value = "${aa.bb}") String name) {
System.out.println("name = " + name);
}
参数名使用了表达式,那么 resolveEmbeddedValuesAndExpressions
方法的目的就是解析出表达式的值,如果没用到表达式,那么该方法会将原参数原封不动返回。
- 4. 接下来调用
resolveName
方法解析出参数的具体值,这个方法也是一个模版方法,具体的实现在子类中。 - 5. 如果获取到的参数值为 null,先去看注解中有没有默认值,然后再去看参数值是否是必须的,如果是,则抛异常出来,否则就设置为 null 即可。
- 6. 如果解析出来的参数值为空字符串
""
,则也去resolveEmbeddedValuesAndExpressions
方法中走一遭。 - 7. 最后则是
WebDataBinder
的处理,解决一些全局参数的问题,WebDataBinder
之前的文章中也有说过。
大致的流程就是这样。
在这个流程中,我们看到主要有如下两个方法是在子类中实现的:
createNamedValueInfo
resolveName
在加上 supportsParameter
方法,子类中一共有三个方法需要我们重点分析。
那么接下来我们就以 RequestParamMethodArgumentResolver
为例,来看下这三个方法。
RequestParamMethodArgumentResolver
![](http://www.enmalvi.com/wp-content/uploads/2022/09/image-79.png)
![](http://www.enmalvi.com/wp-content/uploads/2022/09/image-78.png)
supportsParameter
方法
@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;
}
}
}
public static boolean isSimpleProperty(Class<?> type) {
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
}
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
从 supportsParameter
方法中可以非常方便的看出支持的参数类型:
- 首先参数如果有
@RequestParam
注解的话,则分两种情况:参数类型如果是 Map,则@RequestParam
注解必须配置 name 属性,否则不支持;如果参数类型不是 Map,则直接返回 true,表示总是支持(想想自己平时使用的时候是不是这样)。 - 参数如果含有
@RequestPart
注解,则不支持。 - 检查下是不是文件上传请求,如果是,返回 true 表示支持。
- 如果前面都没能返回,则使用默认的解决方案,判断是不是简单类型,主要就是 Void、枚举、字符串、数字、日期等等。
这块代码其实很简单,支持谁不支持谁,一目了然。
createNamedValueInfo
方法
@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 {
public RequestParamNamedValueInfo() {
super("", false, ValueConstants.DEFAULT_NONE);
}
public RequestParamNamedValueInfo(RequestParam annotation) {
super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
获取注解,读取注解中的属性,构造 RequestParamNamedValueInfo
对象返回。
resolveName
技术
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
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);
}
}
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
这个方法思路也比较清晰:
- 前面两个 if 主要是为了处理文件上传请求。
- 如果不是文件上传请求,则调用
request.getParameterValues
方法取出参数返回即可。