Spring 请求参数类型转换解析(@DateTimeFormat 、自定义Convert)

26 篇文章 1 订阅

Spring 请求参数类型转换解析(@DateTimeFormat 、自定义Convert)

在上节 Spring 之请求参数解析原理 中有说到关于参数的类型转换是依靠 WebDataBinder(数据绑定器,进行数据绑定的工作)中的 conversionService(负责数据类型的转换和格式化工作 )中的各个converters (负责各种 数据类型的转换 工作)来处理的,这节来说说它~

前言

在定义一个接口时,有很多种方式来实现接口的参数接收,常用的有以下三种:

  • 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很好用,配置一下即可
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值