HTTP请求中携带的queryString和form-data数据(文件除外)都是是String类型。那么在Controller上怎么可以直接指定数据类型呢。其实是Spring默认帮我们做了类型转化。
内置数据类型转换器介绍
Converter<S, T>
- String -> Integer
@GetMapping("/age")
@ResponseBody
public String age(Integer age) {
System.out.println(age);
return "OK";
}
例如这个接口,我们用Postman发送数据,如图:
解析过程中String转化成了Integer
从convertIfNecessary追进去,发现最终使用的是 org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter,它内部对org.springframework.core.convert.converter.Converter进行了代理。
我们看org.springframework.core.convert.converter.Converter都有哪些实现类,看到有一个org.springframework.core.convert.support.StringToNumberConverterFactory.StringToNumber,看它的convert方法
private static final class StringToNumber<T extends Number> implements Converter<String, T> {
// 具体的数字类,integer、Float、BigDecimal等
private final Class<T> targetType;
public StringToNumber(Class<T> targetType) {
this.targetType = targetType;
}
@Override
@Nullable
public T convert(String source) {
if (source.isEmpty()) {
return null;
}
// 根据具体数字类型做转换
return NumberUtils.parseNumber(source, this.targetType);
}
}
因为数字类型有很多种,如integer、Float、BigDecimal等,它们都继承自Number类,所以StringToNumber的泛型使用的是Number,而不是具体的Integer、Float。使用时,根据内部属性targetType判断要创建什么类型的转换器。需要注意像这种工厂形式转换器,Spring并没有缓存具体的转换器对象,只是缓存了工厂对象。
2. String -> Enum
假设我们有一个枚举类
public enum Gender {
FEMAL(1, "女性"),
MALE(2, "男性")
;
private int code;
private String desc;
Gender(int code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
}
我们的Controller如下
@GetMapping("/gender")
@ResponseBody
public String gender(@RequestParam Gender gender) {
System.out.println(gender);
return "OK";
}
类似上面的套路我们发现有一个org.springframework.core.convert.support.StringToEnumConverterFactory类,内部有一个StringToEnum静态类。转化过程是用到了Enum类自身带的方法。
@Override
@Nullable
public T convert(String source) {
if (source.isEmpty()) {
// It's an empty enum identifier: reset the enum value to null.
return null;
}
return (T) Enum.valueOf(this.enumType, source.trim());
}
此时,String类型的参数就能转化成Enum。
我们知道Enum不仅有String类型的name属性,还有int类型的ordinal,哪是不是也有一个类似IntToEnum的转换器呢,找一下,确实有org.springframework.core.convert.support.IntegerToEnumConverterFactory,是根据Enum的ordinal转换成Enum。但是注意,Postman传的参数都是String类型的,不会用到这个IntegerToEnumConverterFactory,也就是说如果像下面这样传参数是不能正确转换的:
所以我们需要自己写一个转换器,把字符串格式的数字转成枚举
public class MyIntegerToGender implements Converter<String, Gender> {
@Override
public Gender convert(String source) {
final Gender[] values = Gender.values();
for (int i = 0; i < values.length; i++) {
if (values[i].getCode() == Integer.valueOf(source)) {
return values[i];
}
}
return null;
}
}
在SpringBoot环境下,只需要把MyIntegerToGender 注册成Bean即可生效,在SpringMVC环境下需要如下配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new MyIntegerToGender());
}
}
此时就可以把前端传递的数字格式的参数转成枚举了。
我们发现还有一个StringToBooleanConverter,里面对String和Boolean的映射做了一些约定,这就是为什么我们输入no字符串,得到的Boolean却是false。
trueValues.add("true");
trueValues.add("on");
trueValues.add("yes");
trueValues.add("1");
falseValues.add("false");
falseValues.add("off");
falseValues.add("no");
falseValues.add("0");
GenericConverter
如果我想实现String和List,String和Set之间的转换,用Converter<S, T>就不太合适了,Converter<S, T>可以进行两种确定类型之间的转换,但不适合带泛型的集合,毕竟集合中的元素甚至都可以不同。
GenericConverter与Converter相比有两点不同
- 支持集合、数组之间互转
- 能够拿到source和target的类信息之外的信息,如注解信息
支持集合、数组之间互转
@GetMapping("/listStr")
@ResponseBody
public String listStr(@RequestParam List<String> strings) {
strings.forEach(System.out::println);
return "ok";
}
可以看到成功以集合的形式接收。
GenericConverter底层依赖的还是Converter,一般 GenericConverter的实现类中都有一个ConversionService对象,这个对象持有所有Converter, GenericConverter的转换工作,最终通过ConversionService转发给了具体的Converter。比如我们只添加了MyIntegerToGender 用于单个枚举类型的转换,如果此时我们这样写,也会成功赋值:
@GetMapping("/listGender")
@ResponseBody
public String listGender(@RequestParam List<Gender> genders) {// 用集合接收参数
genders.forEach(System.out::println);
return "ok";
}
能够拿到source和target的类信息之外的信息,如注解信息
因为从HTTP请求中QueryString 、 form-data、x-www-form-urlencoded中解析出的原始数据都是String类型的,默认是没有StringToDate的转换器的,但是我们可以这样接收Date参数:
@GetMapping("/date")
@ResponseBody
public String date(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
System.out.println(date.getTime());
return "OK";
}
如果不带@DateTimeFormat注解会报错。Spring在根据源类型和目标类型查找合适转换器时,会参考参数上的注解信息。这里使用的是ParserConverter,它内部有一个Parser,负责把String按一定的格式输出。有小伙伴一直纠结Converter和Formatter的区别,其实Formatter = Converter + 格式。即按照一定的格式转换。
3. String -> 文件
按照上面的套路,如果有这么一个接口,我们可能会想到,使用的是StringToFileConverter.
@GetMapping("/file")
@ResponseBody
public String file(@RequestParam File file) {
System.out.println(file);
return "ok";
}
确实有这么一个转换器,但它是Spring内部使用的,没有暴露给我我们。这个接口使用的其实是ObjectToObjectConverter。这个转化器会根据sourceClass和targetClass,查找有没有方法或者构造函数能让source变成target。
题外话
@RequestParam和@RequestPart都可以接收MultipartFile,前者是走Converter流程,后者是走HttpMessageConverter流程。当方法中直接用MultipartFile作为参数时,两个注解都可以正确接收,因为此时不需要数据类型转化。但是如果用String接收,@RequestParam就会接收失败,因为没有MultipartFile和String之间的转换器,但是@RequestPart可以使用String类型接收,因为StringHttpMessageConverter做了MultipartFile和String之间的转换。
小结
本文介绍了@RequestParam注解下常见的数据格式转换,这些转换大部分是Spring默认帮我们做的,使用者几乎是无感知的,但是有时当我们会遇到需要自定义的场景。