Spring数据类型转化

HTTP请求中携带的queryString和form-data数据(文件除外)都是是String类型。那么在Controller上怎么可以直接指定数据类型呢。其实是Spring默认帮我们做了类型转化。

内置数据类型转换器介绍

Converter<S, T>

  1. 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相比有两点不同

  1. 支持集合、数组之间互转
  2. 能够拿到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默认帮我们做的,使用者几乎是无感知的,但是有时当我们会遇到需要自定义的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值