Spring 请求参数类型转换解析(@DateTimeFormat 、自定义Convert)
在上节 Spring 之请求参数解析原理 中有说到关于参数的类型转换是依靠 WebDataBinder(数据绑定器,进行数据绑定的工作)中的 conversionService(负责数据类型的转换和格式化工作 )中的各个converters (负责各种 数据类型的转换 工作)来处理的,这节来说说它~
关于Spring的核心元素 DispatchServlet 请查看:Spring 原理之 DispatchServlet 原理
关于 Spring 之请求参数解析原理(实体类传参解析)请查看 Spring 之请求参数解析原理(实体类传参解析)
前言
在定义一个接口时,有很多种方式来实现接口的参数接收,常用的有以下三种:
request 作为接口的方法参数,然后request 根据 key获取传递的参数值
@GetMapping("/request_getValue") public ResponseEntity<User> requestGetValue(HttpServletRequest request){ // request 根据 key 获取值 String username = request.getParameter("username"); String name = request.getParameter("name"); User user = new User(); user.setName(name); user.setUsername(username); return new ResponseEntity<>(user , HttpStatus.OK); }
直接以方法参数的方式进接收传递的参数值
// 直接以参数的形式获取参数值 @GetMapping("/param_getValue") public ResponseEntity<User> paramGetValue(String username, String name){ User user = new User(); user.setName(name); user.setUsername(username); return new ResponseEntity<>(user , HttpStatus.OK); }
利用 Pojo 类 以方法参数的方式来封装获取参数值
@GetMapping("/pojo_getValue") public ResponseEntity<User> pojoGetValue(User user){ return new ResponseEntity<>(user , HttpStatus.OK); }
在上述调用接口的过程中,接口中参数的类型 与 调用接口传递数据的数据类型,会涉及到类型转换,而这些类型转换都由 WebDataBinder(数据绑定器,进行数据绑定的工作)中的 conversionService(负责数据类型的转换和格式化工作 )中的各个 converters (负责各种 数据类型的转换 工作)来处理的
converters 默认是一个大小为 124 的HashMap,key 为 转换前类型->转换后类型 字符串,value 为 org.springframework.core.convert.converter.ConverterFactory 的实现类,相互对应完成数据的转换,部分举例如下:
示例
1、接口实现
定义一个接口,一个参数,名称为 id ,类型为 Integer
@GetMapping("/test_getValue")
public ResponseEntity<User> testGetValue(Integer id) {
return new ResponseEntity<>(HttpStatus.OK);
}
2、接口调用
正常调用:
http://localhost:8081/test_getValue?id=1
错误调用:
http://localhost:8081/test_getValue?id=ahhaha
报错:
3、结论
可以看到,我们传递的参数的类型转换,其是自动帮我们进行转换,根据传递的参数类型与接口定义的参数类型自行选择合适的converter进行转换,仅当不能进行转换的时候,才会报错:Caused by: java.lang.xxxxFormatException
4、原理
在 Spring 之请求参数解析原理(实体类传参解析) 这一篇博客中,跟踪源码,参数解析时的核心处理方法:
- org.springframework.core.convert.support.GenericConversionService#convert
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
// 获取到converter
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
// 核心处理方法
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
// 根据 sourceType 与 targetType 获取converter,其实就是根据key在hashMap中获取到Value(xxxConverterFactory)
@Nullable
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
- org.springframework.core.convert.support.ConversionUtils#invokeConverter
@Nullable
public static Object invokeConverter(GenericConverter converter, @Nullable Object source,
TypeDescriptor sourceType, TypeDescriptor targetType) {
// 调用找到的converter的convert方法来真正进行转换
return converter.convert(source, sourceType, targetType);
}
这里以 String---->Number 为例,由 ConverterFactory 的实现类 StringToNumberConverterFactory 来处理,对应源码如下:
final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
@Override
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToNumber<>(targetType);
}
private static final class StringToNumber<T extends Number> implements Converter<String, T> {
private final Class<T> targetType;
public StringToNumber(Class<T> targetType) {
this.targetType = targetType;
}
// source:接口传参的参数值
@Override
public T convert(String source) {
if (source.isEmpty()) {
return null;
}
// 调用 NumberUtils中的parseNumber进行转换
return NumberUtils.parseNumber(source, this.targetType);
}
}
}
- org.springframework.util.NumberUtils#parseNumber
// parse方法,根据 targetClass 的类型进行值得转换,当都不满足时抛错
public static <T extends Number> T parseNumber(String text, Class<T> targetClass) {
Assert.notNull(text, "Text must not be null");
Assert.notNull(targetClass, "Target class must not be null");
String trimmed = StringUtils.trimAllWhitespace(text);
if (Byte.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed));
}
else if (Short.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed));
}
else if (Integer.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed));
}
else if (Long.class == targetClass) {
return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed));
}
else if (BigInteger.class == targetClass) {
return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed));
}
else if (Float.class == targetClass) {
return (T) Float.valueOf(trimmed);
}
else if (Double.class == targetClass) {
return (T) Double.valueOf(trimmed);
}
else if (BigDecimal.class == targetClass || Number.class == targetClass) {
return (T) new BigDecimal(trimmed);
}
else {
throw new IllegalArgumentException(
"Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]");
}
}
5、总结
- 类型转换是Spring自己帮我们做的,我们无需进行处理,只有其默认得124个converter都不能处理的时候,抛出错误
- 类型转换的底层原理:通过调用 ConverterFactory 的实现类(converter HashMap中 key 对应的value值)中的Converter 实现类的convert方法来处理
LocalDate、LocalDateTime
在实际应用中,有一种特殊的类型LocalDate、LocalDateTime的转换
示例
1、接口定义
@GetMapping("/test_localDate")
public ResponseEntity<User> localDateValue(LocalDate date) {
System.out.println(date);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/test_dateTime")
public ResponseEntity<User> dateValue(LocalDateTime date) {
System.out.println(date);
return new ResponseEntity<>(HttpStatus.OK);
}
2、接口调用
http://localhost:8081/test_dateTime?date=2022-11-24
http://localhost:8081/test_localDate?date=2022-11-24
3、结果
这里就有疑问了,为什么传递的是 value ‘2022-11-24’ 就是对应LocalDate的字符串呀,怎么不能转换呢?
根据上述的原理我们去看一下为什么(LocalDate为例):
- 首先找 java.lang.String -> java.time.LocalDate 的转换 converter,根据调试得知 org.springframework.format.support.FormattingConversionService
- 然后查看其convert方法的核心逻辑
源码:
- org.springframework.format.support.FormattingConversionService#convert
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String text = (String) source;
if (!StringUtils.hasText(text)) {
return null;
}
// 这里的parser为org.springframework.format.datetime.standard.TemporalAccessorParser 通过parse方法来完成转换
Object result=this.parser.parse(text, LocaleContextHolder.getLocale());
TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
if (!resultType.isAssignableTo(targetType)) {
result = this.conversionService.convert(result, resultType, targetType);
}
return result;
}
- org.springframework.format.datetime.standard.TemporalAccessorParser#parse
// text:传递的参数值,这里对应为2022-11-24
//
@Override
public TemporalAccessor parse(String text, Locale locale) throws ParseException {
DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale);
// temporalAccessorType 为 转换后的类型,这里 java.lang.String -> java.time.LocalDate,所以为 LocalDate
if (LocalDate.class == this.temporalAccessorType) {
// formatterToUse 为 (Localized(SHORT,))
return LocalDate.parse(text, formatterToUse);
}
else if (LocalTime.class == this.temporalAccessorType) {
return LocalTime.parse(text, formatterToUse);
}
else if (LocalDateTime.class == this.temporalAccessorType) {
return LocalDateTime.parse(text, formatterToUse);
}
else if (ZonedDateTime.class == this.temporalAccessorType) {
return ZonedDateTime.parse(text, formatterToUse);
}
else if (OffsetDateTime.class == this.temporalAccessorType) {
return OffsetDateTime.parse(text, formatterToUse);
}
else if (OffsetTime.class == this.temporalAccessorType) {
return OffsetTime.parse(text, formatterToUse);
}
else {
throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType);
}
}
可以看到这里的 formatter 的 parser 为 (Localized(SHORT,)) ,即支持年月日short的形式,例如: 22-5-23
即若调用以下则为成功调用:
http://localhost:8081/test_localDate?date=22-11-24
不难发现,其实可以进行类型转换,要满足其的 formatter 中的 parser 规则才行,那么如何自定义自己的pattern呢?(我就想以2022-11-24来传)
两种方式:
- 使用 @DateTimeFormat
- 自定义 Converter
@DateTimeFormat
原理:这个就是改变上述formatter 中的 parser 规则,使其可以处理数据
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DateTimeFormat {
/**
* 默认 'SS' for short date time. 例如 22-5-13 12-2-3
*/
String style() default "SS";
/**
* 使用标准的iso相关的来处理,取值如下面的enum ISO
*/
ISO iso() default ISO.NONE;
/**
* 自定义pattern
*/
String pattern() default "";
/**
* ISO date time format patterns
*/
enum ISO {
/**
* yyyy-MM-dd, e.g. "2000-10-31".
*/
DATE,
/**
* HH:mm:ss.SSSXXX, e.g. "01:30:00.000-05:00".
*/
TIME,
/**
* yyyy-MM-dd'T'HH:mm:ss.SSSXXX, e.g. "2000-10-31T01:30:00.000-05:00".
*/
DATE_TIME,
/**
* 无iso
*/
NONE
}
}
1、接口修改,参数增加 @DateTimeFormat 注解,制定 parse 规则,两种方式,原理相同
// 使用iso
@GetMapping("/test_localDate")
public ResponseEntity<User> localDateValue(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
System.out.println(date);
return new ResponseEntity<>(HttpStatus.OK);
}
// 使用pattern
@GetMapping("/test_localDate")
public ResponseEntity<User> localDateValue( @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
System.out.println(date);
return new ResponseEntity<>(HttpStatus.OK);
}
2、接口调用
http://localhost:8081/test_localDate?date=2022-11-24
3、结果(正常调用,无错误产生)
4、原理
使用默认的 ISO 8601 格式化 (yyyy-MM-dd),通过 @DateTimeFormat注解并设置其属性为 DateTimeFormat.ISO.DATE,原理就是修改这里的 parser ,让其可以处理 parse 2022-11-24 的数据
自定义 Converter
上述的方法虽然简单,但是只要在使用了LocalDate、LocaDate参数的接口中,都要用 @DateTimeFormat 注解,未免有点太麻烦~
下面介绍另外一种一劳永逸的方法:自定义 Converter。在上述总结的类型转换的底层原理是通过调用 ConverterFactory 的实现类(converter HashMap中 key 对应的value值)中 Converter 实现类的 **convert **方法来处理
那么我们通过自定义实现 ConverterFactory 来完成我们自己希望的类型转换,主要涉及三个步骤:
- 自定义 Converter 类,实现 Converter 接口,复写convert() 方法,完成自己的需求
- 注入自定义 Converter 类
以 java.lang.String -> java.time.LocalDate 的转换为例来举例说明:
自定义 Converter 类,实现 Converter 接口
编写 Converter 的实现类 LocalDateConverter 完成 String ====》LocalDate 的转换
package com.study.config;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public final class LocalDateConverter implements Converter<String, LocalDate> {
private final DateTimeFormatter formatter;
/**
* 根据pattern
*
* @param dateFormat
*/
public LocalDateConverter(String dateFormat) {
this.formatter = DateTimeFormatter.ofPattern(dateFormat);
}
/**
* 根据ISO来指定
*
* @param iso
*/
public LocalDateConverter(DateTimeFormat.ISO iso) {
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
switch (iso) {
case DATE:
formatter = DateTimeFormatter.ISO_DATE;
case DATE_TIME:
formatter = DateTimeFormatter.ISO_DATE_TIME;
default:
formatter = DateTimeFormatter.ISO_DATE;
}
this.formatter = formatter;
}
@Override
public LocalDate convert(String source) {
if (source.isEmpty()) {
return null;
}
return LocalDate.parse(source, formatter);
}
}
注入自定义 Converter 类
注入 Formatters ,传入自定义的pattern / iso
package com.study.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Configuration
public class MyWebMvcSupport extends WebMvcConfigurationSupport {
@Override
protected void addFormatters(FormatterRegistry registry) {
registry.addConverter(new LocalDateConverter("yyyy-MM-dd"));
//或者
//registry.addConverter(new LocalDateConverter(DateTimeFormat.ISO.DATE));
}
}
1、接口修改,正常编写,无需注解
@GetMapping("/test_localDate")
public ResponseEntity<User> localDateValue(LocalDate date) {
System.out.println(date);
return new ResponseEntity<>(HttpStatus.OK);
}
2、接口调用
http://localhost:8081/test_localDate?date=2022-11-24
3、结果(正常调用,无错误产生)
4、原理
上面的解析是通过 org.springframework.format.support.FormattingConversionService$ParserConverter 的convert 方法完成的转换,而这里调试后的convert就为我们上面编写的 LocalDateConverter 完成转换,即改变实现类型转换的convert来实现具体的业务
总结
- @DateTimeFormat 注解主要可用于以下类型:java.util.Date, java.util.Calendar, java.lang.Long, Joda-Time 值类型,从spring 4和 jdk8 开始,到 JSR-310 java.time 类型都能支持,通过指定@DateTimeFormat 注解的iso属性值、pattern 属性值来完成date的格式需求
- 自定义Converter很好用,配置一下即可