@RequestParam注解的使用及源码解析

前言

@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

参数的处理分为两个阶段:

  1. 判断当前环境中存在的resolvers,是否支持解析当前参数
  2. 处理参数

判断是否支持解析当前参数

我的环境中存在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:支持
  • 不存在 @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 方法大家可以自行阅读,相关源码如下:

大概分为以下五个步骤:

  1. 构建NamedValueInfo对象
  2. 处理Spel表达式
  3. 解析参数
  4. 处理默认值
  5. 类型转换
构建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

总体分为三个优先级:

  1. HttpServletRequest 类型为 MultipartHttpServletRequest 或 contentType 以 multipart/ 开头,则处理 MultipartFile (Part)类型的传参
  2. HttpServletRequest 类型为 MultipartRequest 则处理 MultipartFile 类型的参数
  3. 将 URL 或 body 中的传参,以 String (String[])返回 (如果存在相应的 convert,则进行类型转换)

PS : 默认情况下,如果 request 的 contentType 以 multipart/ 开头,SpringBoot 会将请求封装成 StandardMultipartHttpServletRequest,它是 MultipartHttpServletRequest 的子类

处理默认值

处理默认值的两种情况

  1. 参数解析结果为 null,@RequestParam 注解的 required 属性为 false,并且设置了默认值
  2. 参数解析结果为空字符串,并且设置了默认值 (尝试以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();
}
相关推荐
柯3491 小时前
GC垃圾回收
java·jvm·垃圾回收
redemption_21 小时前
SpringMVC-03-HelloSpring
java
平头哥在等你1 小时前
C语言简答题答案
java·c语言·jvm
LKID体1 小时前
【python图解】数据结构之字典和集合
java·服务器·前端
CopyLower2 小时前
深入理解 MyBatis 的缓存机制:一级缓存与二级缓存
spring·缓存·mybatis
Iced_Sheep2 小时前
Spring @Transactional 你真的会用吗???
后端·spring
HUT_Tyne2652 小时前
力扣--LCR 154.复杂链表的复制
java·leetcode·链表
黄昏_2 小时前
在Springboot项目中实现将文件上传至阿里云 OSS
java·spring boot·后端·阿里云
写bug写bug2 小时前
用Java Executors创建线程池的9种方法
java·后端
期待未来的男孩2 小时前
安全加固方案
java·网络·安全