前言
@RequestParam 注解是我们进行JavaEE开发,最常见的几个注解之一,这篇博文我们以案例和源码相结合,帮助大家更好的了解@RequestParam 注解
使用案例
1.获取 URL 上的值
@GetMapping("/simple")
public String simple(@RequestParam(value = "name") String name) {
return name;
}
2.获取 URL 上的值,如果不存在,使用默认值
@GetMapping("/default")
public String defaultValue(@RequestParam(value = "name", defaultValue = "hello world") String name) {
return name;
}
3.URL 上存在多个指定的 KEY
@GetMapping("/list")
public String list(@RequestParam(value = "name") List<String> names) {
return names.toString();
}
PS : 同样可以使用Set、Collection接收,只要是Collection或其子类都可以
4.使用 Map 接收
4.1 使用接口 Map 接收
@GetMapping("/map")
public String map(@RequestParam Map<String, Object> map) {
return map.toString();
}
4.2 使用 MultiValueMap 接收
@GetMapping("/multi_value")
public String map(@RequestParam MultiValueMap<String, Object> map) {
return map.toString();
}
5.接收文件
5.1 接收单个文件
@GetMapping("/file")
public String multipart(@RequestParam(value = "file") MultipartFile file) {
return file.getOriginalFilename();
}
5.2 接收多个文件
@GetMapping("/multi_file")
public String multipart(@RequestParam(value = "file") List<MultipartFile> files) {
return files.stream().map(MultipartFile::getOriginalFilename).collect(Collectors.joining(","));
}
6.其他
6.1 不存在 @RequestParam 注解
@GetMapping("/none")
public String none(String name) {
return StringUtils.isBlank(name) ? "none" : name;
}
PS : 效果类似上文中的案例2
6.2 Spel表达式
6.2.1 ${}
创建 keys.properties
key=a
引用 keys.properties
@SpringBootApplication
@PropertySource("classpath:keys.properties")
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class);
}
}
接口及响应
@GetMapping("/spel1")
public String spel1(@RequestParam(value = "${key}") String name) {
return name;
}
6.2.1 #{}
创建 RequestKey
@Component
public class RequestKey {
private String key = "b";
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
接口及响应
@GetMapping("/spel2")
public String spel2(@RequestParam(value = "#{requestKey['key']}") String name) {
return name;
}
源码解析
InvocableHandlerMethod#getMethodArgumentValues
参数的处理分为两个阶段:
- 判断当前环境中存在的resolvers,是否支持解析当前参数
- 处理参数
判断是否支持解析当前参数
我的环境中存在27个resolvers,通过命名我们大概可以猜测出 RequestParamMethodArgumentResolver、RequestParamMapMethodArgumentResolver 是处理 @RequestParam 注解的 resolver
RequestParamMethodArgumentResolver#supportsParameter
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
// 存在RequestParam注解,返回类型是Map,并且指定了name
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
} else {
// 存在@RequestParam注解,并且返回类型不是Map
return true;
}
} else {
// 不存在@RequestPart注解
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
// 返回类型是 MultipartFile 或者 MultipartFile集合、MultipartFile数组
// 返回类型是 Part 或者 Part集合、Part数组
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
// 如果useDefaultResolution属性为true,即使不存在@RequestParam注解,也可以处理普通类
} else if (this.useDefaultResolution) {
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
} else {
return false;
}
}
}
通过上述源码,我们得出以下结论:
- 存在 @RequestParam 注解
- 返回类型是Map
- 指定name:支持
- 未指定name:不支持
- 返回类型非Map:支持
- 返回类型是Map
- 不存在 @RequestParam 注解
- 存在 @RequestPart 注解 :不支持
- 不存在 @RequestPart 注解
- 参数类型是否是MultipartFile(Part):支持
- useDefaultResolution属性是否为true,并且参数类型是普通类 : 支持
- 其他情况 : 不支持
PS : 所以 RequestParamMethodArgumentResolver 也可以处理上文中没有 @RequestParam 注解的情况(案例6.1)。通过下方的截图我们可以发现,存在两个类型都为 RequestParamMethodArgumentResolver 的 resolver ,其中一个的 useDefaultResolution 属性为 true,这个 resolver 就是用来处理没有 @RequestParam 注解,并满足一定条件的传入参数
Spring中定义的普通类
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));
}
RequestParamMapMethodArgumentResolver#supportsParameter
@Override
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(requestParam.name()));
}
RequestParamMapMethodArgumentResolver 的 supportsParameter 方法比较简单,只能处理满足下面三个条件的参数:
- 存在 @RequestParam 注解
- 返回类型是 Map
- 未指定 value (name)
处理参数
接来下我们将重点分析 RequestParamMethodArgumentResolver 的 resolveArgument 方法,RequestParamMapMethodArgumentResolver 的 resolveArgument 方法大家可以自行阅读,相关源码如下:
大概分为以下五个步骤:
- 构建NamedValueInfo对象
- 处理Spel表达式
- 解析参数
- 处理默认值
- 类型转换
构建NamedValueInfo对象
创建NamedValueInfo对象
如果存在 @RequestParam 注解,则使用自定义的值,否则就使用默认值,通过上述源码,我们可以得知:
- @RequestParam 注解的 required 属性的默认值是 true
- NamedValueInfo 对象的 required 属性的默认值是 false,所以针对案例 6.1,不传相应参数也不会抛出异常
更新NamedValueInfo对象
updateNamedValueInfo 方法主要针对不存在@RequestParam 注解,NamedValueInfo对象的 name 属性值为方法的参数名
处理Spel表达式
主要对Spel表达式进行解析,比如案例 6.2.1 中 ${key} 会被解析成 a ,案例 6.2.2 中 #{requestKey['key']} 会被解析成 b
解析参数
RequestParamMethodArgumentResolver#resolveName
总体分为三个优先级:
- HttpServletRequest 类型为 MultipartHttpServletRequest 或 contentType 以 multipart/ 开头,则处理 MultipartFile (Part)类型的传参
- HttpServletRequest 类型为 MultipartRequest 则处理 MultipartFile 类型的参数
- 将 URL 或 body 中的传参,以 String (String[])返回 (如果存在相应的 convert,则进行类型转换)
PS : 默认情况下,如果 request 的 contentType 以 multipart/ 开头,SpringBoot 会将请求封装成 StandardMultipartHttpServletRequest,它是 MultipartHttpServletRequest 的子类
处理默认值
处理默认值的两种情况
- 参数解析结果为 null,@RequestParam 注解的 required 属性为 false,并且设置了默认值
- 参数解析结果为空字符串,并且设置了默认值 (尝试以Spel表达式的方式进行解析)
类型转换
SpringBoot 会提前内置很多 convert,当存在一个 convert 可以将当前类型转换为目标类型,则会进行转换。比如案例3中,需要一个将 String 数组转换为 LIst 的 convert,因为该 convert (上图框中的 convert)存在,所以我们可以用 List (当前类型和目标类型与 convert 类型一致或是其子类都可以)去接收参数。
自定义Convert
除了系统内置的 convert,我们也可以自定义 convert,案例演示如下:
创建配置类 WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Dog>() {
@Override
public <U> Converter<String, U> andThen(Converter<? super Dog, ? extends U> after) {
return Converter.super.andThen(after);
}
@Override
public Dog convert(String source) {
return new Dog(source);
}
});
}
}
创建实体类 Dog
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
接口及响应
@GetMapping("/convert")
public String convert(@RequestParam(value = "name") Dog dog) {
return dog.toString();
}