项目中使用LocalDateTime系列作为DTO中时间的数据类型,但是SpringMVC收到参数后总报错,为了配置全局时间类型转换,尝试了如下处理方式。
注:本文基于Springboot2.x测试,如果无法生效可能是spring版本较低导致的。PS:如果你的Controller中的LocalDate类型的参数啥注解(RequestParam、PathVariable等)都没加,也是会出错的,因为默认情况下,解析这种参数是使用**ModelAttributeMethodProcessor**进行处理,而这个处理器要通过反射实例化一个对象出来,然后再对对象中的各个参数进行convert,但是LocalDate类没有构造函数,无法反射实例化因此会报错!!!

完成目标

  • 请求入参为 String(指定格式)转 Date,支持get、post(content-type=application/json)
  • 返回数据为Date类型转为指定的日期时间格式字符创
  • 支持Java8 日期 API,如:LocalTime、localDate 和 LocalDateTime

    GET请求及POST表单日期时间字符串格式转换

    这种情况要和时间作为Json字符串时区别对待,因为前端json转后端pojo底层使用的是Json序列化Jackson工具(HttpMessgeConverter);而时间字符串作为普通请求参数传入时,转换用的是Converter,两者在处理方式上是有区别。

    使用自定义参数转换器(Converter)

    实现 org.springframework.core.convert.converter.Converter,自定义参数转换器,如下:

    1. @Configuration
    2. public class DateConverterConfig {
    3. @Bean
    4. public Converter<String, LocalDate> localDateConverter() {
    5. return new Converter<String, LocalDate>() {
    6. @Override
    7. public LocalDate convert(String source) {
    8. return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    9. }
    10. };
    11. }
    12. @Bean
    13. public Converter<String, LocalDateTime> localDateTimeConverter() {
    14. return new Converter<String, LocalDateTime>() {
    15. @Override
    16. public LocalDateTime convert(String source) {
    17. return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    18. }
    19. };
    20. }
    21. }

    小结:

  • GET请求及POST表单方式请求。

  • 支持LocalDate等Java8日期API。

点评:以上两个bean会注入到spring mvc的参数解析器(好像叫做ParameterConversionService),当传入的字符串要转为LocalDateTime类时,spring会调用该Converter对这个入参进行转换。
注意:关于自定义的参数转换器 Converter,这里我遇到了一个坑,我再这里详细记录下,本来我的想法是为了代码精简,将上面匿名内部类的写法精简成lambda表达式的方式:

  1. @Bean
  2. // @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  3. public Converter<String, LocalDate> localDateConverter() {
  4. return source -> LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
  5. }

当我再次启动项目时却出现了异常:

  1. Caused by: java.lang.IllegalArgumentException: Unable to determine source type <S> and target type <T> for your Converter [com.example.demo126.config.MappingConverterAdapter$$Lambda$522/817994751]; does the class parameterize those types?

百思不得其解,在查阅了资料才得知一二:
web项目启动注册requestMappingHandlerAdapter的时候会初始化WebBindingInitializer

  1. adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());

而ConfigurableWebBindingInitializer需要FormattingConversionService, 而FormattingConversionService会将所有的Converter添加进来,添加的时候需要获取泛型信息:

  1. @Override
  2. public void addFormatters(FormatterRegistry registry) {
  3. for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
  4. registry.addConverter(converter);
  5. }
  6. for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
  7. registry.addConverter(converter);
  8. }
  9. for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
  10. registry.addFormatter(formatter);
  11. }
  12. }

添加Converter.class 一般是通过接口获取两个泛型的具体类型

  1. public ResolvableType as(Class<?> type) {
  2. if (this == NONE) {
  3. return NONE;
  4. }
  5. Class<?> resolved = resolve();
  6. if (resolved == null || resolved == type) {
  7. return this;
  8. }
  9. for (ResolvableType interfaceType : getInterfaces()) {
  10. ResolvableType interfaceAsType = interfaceType.as(type);
  11. if (interfaceAsType != NONE) {
  12. return interfaceAsType;
  13. }
  14. }
  15. return getSuperType().as(type);
  16. }

Lambda表达式的接口是Converter,并不能得到具体的类型,在窥探了SpringMVC源码后才得知原来如此,既然指导了原因,那解决办法:

  • 最简单的方法就是不适用Lambda表达式,还是老老实实的使用匿名内部类,这样就不会存在上述问题
  • 或者就是等requestMappingHandlerAdapterbean注册完成之后再添加自己的converter就不会注册到FormattingConversionService中
    1. @Bean
    2. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
    3. public Converter<String, LocalDateTime> localDateTimeConverter() {
    4. return source -> LocalDateTime.parse(source, DateTimeUtils.DEFAULT_FORMATTER);
    5. }

    使用Spring注解

    使用spring自带注解@DateTimeFormat(pattern = “yyyy-MM-dd”),如下:
    1. @DateTimeFormat(pattern = "yyyy-MM-dd")
    2. private Date startDate;
    如果使用了自定义参数转化器,Spring会优先使用该方式进行处理,即Spring注解不生效。

那么假如我们使用了自定义参数转换器,但是还是想兼容用yyyy-MM-dd形式接受呢?我们可以把前面的dateConverter改成用正则匹配方式,这样也不失为一种不错的解决方案,示例如下。

  1. /**
  2. * 日期正则表达式
  3. */
  4. private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
  5. /**
  6. * 时间正则表达式
  7. */
  8. private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";
  9. /**
  10. * 日期和时间正则表达式
  11. */
  12. private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;
  13. /**
  14. * 13位时间戳正则表达式
  15. */
  16. private static final String TIME_STAMP_REGEX = "1\\d{12}";
  17. /**
  18. * 年和月正则表达式
  19. */
  20. private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";
  21. /**
  22. * 年和月格式
  23. */
  24. private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
  25. @Bean
  26. public Converter<String, Date> dateConverter() {
  27. return new Converter<String, Date>() {
  28. @SuppressWarnings("NullableProblems")
  29. @Override
  30. public Date convert(String source) {
  31. if (StrUtil.isEmpty(source)) {
  32. return null;
  33. }
  34. if (source.matches(TIME_STAMP_REGEX)) {
  35. return new Date(Long.parseLong(source));
  36. }
  37. DateFormat format;
  38. if (source.matches(DATE_TIME_REGEX)) {
  39. format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
  40. } else if (source.matches(DATE_REGEX)) {
  41. format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
  42. } else if (source.matches(YEAR_MONTH_REGEX)) {
  43. format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
  44. } else {
  45. throw new IllegalArgumentException();
  46. }
  47. try {
  48. return format.parse(source);
  49. } catch (ParseException e) {
  50. throw new RuntimeException(e);
  51. }
  52. }
  53. };
  54. }

或者

  1. @Bean
  2. public Converter<String, Date> dateConverter() {
  3. return new Converter<String, Date>() {
  4. /*
  5. * 可对value进行正则匹配,支持日期、时间等多种类型转换
  6. * 这里我偷个懒,在匹配Date日期格式时直接使用了 hutool 为我们已经写好的解析工具类,这里就不重复造轮子了
  7. * cn.hutool.core.date.DateUtil
  8. * @param value
  9. * @return
  10. */
  11. @SuppressWarnings("NullableProblems")
  12. @Override
  13. public Date convert(String source) {
  14. return DateUtil.parse(source.trim());
  15. }
  16. };
  17. }

注:这里我偷个懒,在匹配Date日期格式时直接使用了 hutool 为我们已经写好的解析工具类,这里就不重复造轮子了,下面的方法同样使用了该工具类,想要在自己的项目中使用该工具类也很简单,在项目pom文件中引入hutool的依赖就可以了,如下:

  1. <!--hu tool 工具类-->
  2. <dependency>
  3. <groupId>cn.hutool</groupId>
  4. <artifactId>hutool-all</artifactId>
  5. <version>5.1.3</version>
  6. </dependency>

小结:

  • GET请求及POST表单方式请求,但是需要在每个使用的地方加上@DateTimeFormat注解。
  • 与自定义参数转化器(Converter)不兼容。
  • 支持LocalDate等Java8日期API。

使用ControllerAdvice配合initBinder

  1. @ControllerAdvice
  2. public class GlobalExceptionHandler {
  3. @InitBinder
  4. protected void initBinder(WebDataBinder binder) {
  5. binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
  6. @Override
  7. public void setAsText(String text) throws IllegalArgumentException {
  8. setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
  9. }
  10. });
  11. binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
  12. @Override
  13. public void setAsText(String text) throws IllegalArgumentException {
  14. setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
  15. }
  16. });
  17. binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
  18. @Override
  19. public void setAsText(String text) throws IllegalArgumentException {
  20. setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
  21. }
  22. });
  23. binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
  24. @Override
  25. public void setAsText(String text) throws IllegalArgumentException {
  26. SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
  27. try {
  28. setValue(formatter.parse(text));
  29. } catch (Exception e) {
  30. throw new RuntimeException(String.format("Error parsing %s to Date", text));
  31. }
  32. }
  33. });
  34. }
  35. }

在实际应用中,我们可以把上面代码放到父类中,所有接口继承这个父类,达到全局处理的效果。原理就是与AOP类似,在参数进入handler之前进行转换时使用我们定义的PropertyEditorSupport来处理。
小结:

  • GET请求及POST表单方式请求。
  • 支持LocalDate等Java8日期API。

    JSON入参及返回值全局处理

    请求类型为:post,content-type=application/json, 后台用@RequestBody接收,默认接收及返回值格式为: yyyy-MM-dd HH:mm:ss

    修改 application.yml 文件

    在application.propertities文件中增加如下内容:

    1. spring:
    2. jackson:
    3. date-format: yyyy-MM-dd HH:mm:ss
    4. time-zone: GMT+8
  • 支持(content-type=application/json)请求中格式为 yyyy-MM-dd HH:mm:ss的字符串,后台用@RequestBody接收,及返回值date转为yyyy-MM-dd HH:mm:ss格式string;

  • 不支持(content-type=application/json)请求中yyyy-MM-dd等类型的字符串转为date;
  • 不支持java8日期api;

    利用Jackson的JSON序列化和反序列化

    1. /**
    2. * Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json
    3. */
    4. @Bean
    5. public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    6. return builder -> builder
    7. .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    8. .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
    9. .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
    10. .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
    11. .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    12. .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
    13. .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
    14. .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
    15. ;
    16. }

    小结:

  • 支持Content-Type 是application/json的POST请求,请求参数字符串和返回的格式都是yyyy-MM-dd HH:mm:ss如果请求参数是其他格式,如yyyy-MM-dd字符串则报400 Bad Request异常。

  • 支持LocalDate等Java8日期API。

PS:上面的方式是通过配置一个Jackson2ObjectMapperBuilderCustomizerBean完成的,除了这种,也可以通过自定义一个MappingJackson2HttpMessageConverter来实现。

  1. @Configuration
  2. public class JacksonConfig {
  3. /** 默认日期时间格式 */
  4. public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  5. /** 默认日期格式 */
  6. public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  7. /** 默认时间格式 */
  8. public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
  9. @Bean
  10. public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
  11. MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  12. ObjectMapper objectMapper = new ObjectMapper();
  13. // 忽略json字符串中不识别的属性
  14. objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  15. // 忽略无法转换的对象
  16. objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
  17. // PrettyPrinter 格式化输出
  18. objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
  19. // NULL不参与序列化
  20. // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
  21. // 指定时区
  22. objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
  23. // 日期类型字符串处理
  24. objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT));
  25. // java8日期日期处理
  26. JavaTimeModule javaTimeModule = new JavaTimeModule();
  27. javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  28. javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  29. javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  30. javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  31. javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  32. javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  33. objectMapper.registerModule(javaTimeModule);
  34. converter.setObjectMapper(objectMapper);
  35. return converter;
  36. }
  37. }

以上几种方式都可以实现JSON传参时的全局化配置,更推荐后两种代码中增加配置bean的方式,可以同时支持Date和LocalDate。

JSON入参及返回值局部差异化处理

场景: 假如全局日期时间处理格式为:yyyy-MM-dd HH:mm:ss,但是某个字段要求接收或返回日期yyyy-MM-dd

方式一 使用@DateTimeFormat和@JsonFormat注解

使用springboot自带的注解@JsonFormat(pattern = “yyyy-MM-dd”),如下所示:

  1. @JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")
  2. @DateTimeFormat(pattern = "yyyy-MM-dd")
  3. private Date originalDate;

如上所示,可以在字段上增加@DateTimeFormat和@JsonFormat注解,可以分别单独指定该字段的接收和返回的日期格式。

PS:@JsonFormat和@DateTimeFormat注解都不是Spring Boot提供的,在Spring应用中也可以使用。

再次提醒,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat注解不生效

方式二 自定义序列化器和反序列化器

  1. /**
  2. * 日期序列化
  3. */
  4. public class DateJsonSerializer extends JsonSerializer<Date> {
  5. @Override
  6. public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  7. SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  8. jsonGenerator.writeString(dateFormat.format(date));
  9. }
  10. }
  11. /**
  12. * 日期反序列化
  13. */
  14. public class DateJsonDeserializer extends JsonDeserializer<Date> {
  15. @Override
  16. public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
  17. try {
  18. SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  19. return dateFormat.parse(jsonParser.getText());
  20. } catch (ParseException e) {
  21. throw new RuntimeException(e);
  22. }
  23. }
  24. }
  25. /**
  26. * 使用方式
  27. */
  28. @JsonSerialize(using = DateJsonSerializer.class)
  29. @JsonDeserialize(using = DateJsonDeserializer.class)
  30. private Date releaseDate;

日期时间格式化处理方式完整配置

方式一

  1. @Configuration
  2. public class DateHandlerConfig {
  3. /** 默认日期时间格式 */
  4. public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  5. /** 默认日期格式 */
  6. public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  7. /** 默认时间格式 */
  8. public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
  9. /**
  10. * LocalDate转换器,用于转换RequestParam和PathVariable参数
  11. * `@ConditionalOnBean(name = "requestMappingHandlerAdapter")`: 等requestMappingHandlerAdapter bean注册完成之后
  12. * 再添加自己的`converter`就不会注册到`FormattingConversionService`中
  13. */
  14. @Bean
  15. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  16. public Converter<String, LocalDate> localDateConverter() {
  17. return source -> LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
  18. }
  19. /**
  20. * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
  21. */
  22. @Bean
  23. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  24. public Converter<String, LocalDateTime> localDateTimeConverter() {
  25. return source -> LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));
  26. }
  27. /**
  28. * LocalTime转换器,用于转换RequestParam和PathVariable参数
  29. */
  30. @Bean
  31. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  32. public Converter<String, LocalTime> localTimeConverter() {
  33. return source -> LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
  34. }
  35. /**
  36. * Date转换器,用于转换RequestParam和PathVariable参数
  37. * 这里关于解析各种格式的日期格式采用了 hutool 的日期解析工具类
  38. */
  39. @Bean
  40. public Converter<String, Date> dateConverter() {
  41. return new Converter<String, Date>() {
  42. @Override
  43. public Date convert(String source) {
  44. return DateUtil.parse(source.trim());
  45. }
  46. };
  47. }
  48. /**
  49. * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
  50. */
  51. @Bean
  52. public ObjectMapper objectMapper(){
  53. ObjectMapper objectMapper = new ObjectMapper();
  54. objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  55. objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
  56. //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
  57. JavaTimeModule javaTimeModule = new JavaTimeModule();
  58. javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  59. javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  60. javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  61. javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
  62. javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
  63. javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  64. // Date序列化和反序列化,會導致@JsonFormat失效
  65. javaTimeModule.addSerializer(Date.class, new JsonSerializer<>() {
  66. @Override
  67. public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  68. SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
  69. String formattedDate = formatter.format(date);
  70. jsonGenerator.writeString(formattedDate);
  71. }
  72. });
  73. javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<>() {
  74. @Override
  75. public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
  76. SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
  77. String date = jsonParser.getText();
  78. try {
  79. return format.parse(date);
  80. } catch (ParseException e) {
  81. throw new RuntimeException(e);
  82. }
  83. }
  84. });
  85. objectMapper.registerModule(javaTimeModule);
  86. return objectMapper;
  87. }
  88. }

方式二

  1. import cn.hutool.core.util.StrUtil;
  2. import com.fasterxml.jackson.databind.deser.std.DateDeserializers;
  3. import com.fasterxml.jackson.databind.ser.std.DateSerializer;
  4. import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
  5. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
  6. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
  7. import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
  8. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
  9. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
  10. import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
  11. import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
  12. import org.springframework.context.annotation.Bean;
  13. import org.springframework.context.annotation.Configuration;
  14. import org.springframework.core.convert.converter.Converter;
  15. import java.text.DateFormat;
  16. import java.text.ParseException;
  17. import java.text.SimpleDateFormat;
  18. import java.time.LocalDate;
  19. import java.time.LocalDateTime;
  20. import java.time.LocalTime;
  21. import java.time.format.DateTimeFormatter;
  22. import java.util.Date;
  23. @Configuration
  24. public class DateHandlerConfig {
  25. /**
  26. * 日期正则表达式
  27. */
  28. private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
  29. /**
  30. * 时间正则表达式
  31. */
  32. private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";
  33. /**
  34. * 日期和时间正则表达式
  35. */
  36. private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;
  37. /**
  38. * 13位时间戳正则表达式
  39. */
  40. private static final String TIME_STAMP_REGEX = "1\\d{12}";
  41. /**
  42. * 年和月正则表达式
  43. */
  44. private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";
  45. /**
  46. * 年和月格式
  47. */
  48. private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
  49. /**
  50. * DateTime格式化字符串
  51. */
  52. private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
  53. /**
  54. * Date格式化字符串
  55. */
  56. private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
  57. /**
  58. * Time格式化字符串
  59. */
  60. private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";
  61. /**
  62. * LocalDate转换器,用于转换RequestParam和PathVariable参数
  63. */
  64. @Bean
  65. public Converter<String, LocalDate> localDateConverter() {
  66. return new Converter<String, LocalDate>() {
  67. @SuppressWarnings("NullableProblems")
  68. @Override
  69. public LocalDate convert(String source) {
  70. if (StrUtil.isEmpty(source)) {
  71. return null;
  72. }
  73. return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
  74. }
  75. };
  76. }
  77. /**
  78. * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
  79. */
  80. @Bean
  81. public Converter<String, LocalDateTime> localDateTimeConverter() {
  82. return new Converter<String, LocalDateTime>() {
  83. @SuppressWarnings("NullableProblems")
  84. @Override
  85. public LocalDateTime convert(String source) {
  86. if (StrUtil.isEmpty(source)) {//使用hutool工具cn.hutool.core.util.StrUtil
  87. return null;
  88. }
  89. return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
  90. }
  91. };
  92. }
  93. /**
  94. * LocalDate转换器,用于转换RequestParam和PathVariable参数
  95. */
  96. @Bean
  97. public Converter<String, LocalTime> localTimeConverter() {
  98. return new Converter<String, LocalTime>() {
  99. @SuppressWarnings("NullableProblems")
  100. @Override
  101. public LocalTime convert(String source) {
  102. if (StrUtil.isEmpty(source)) {
  103. return null;
  104. }
  105. return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN));
  106. }
  107. };
  108. }
  109. /**
  110. * Date转换器,用于转换RequestParam和PathVariable参数
  111. */
  112. @Bean
  113. public Converter<String, Date> dateConverter() {
  114. return new Converter<String, Date>() {
  115. @SuppressWarnings("NullableProblems")
  116. @Override
  117. public Date convert(String source) {
  118. if (StrUtil.isEmpty(source)) {
  119. return null;
  120. }
  121. if (source.matches(TIME_STAMP_REGEX)) {
  122. return new Date(Long.parseLong(source));
  123. }
  124. DateFormat format;
  125. if (source.matches(DATE_TIME_REGEX)) {
  126. format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
  127. } else if (source.matches(DATE_REGEX)) {
  128. format = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
  129. } else if (source.matches(YEAR_MONTH_REGEX)) {
  130. format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
  131. } else {
  132. throw new IllegalArgumentException();
  133. }
  134. try {
  135. return format.parse(source);
  136. } catch (ParseException e) {
  137. throw new RuntimeException(e);
  138. }
  139. }
  140. };
  141. }
  142. /**
  143. * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
  144. */
  145. @Bean
  146. public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
  147. return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
  148. .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
  149. .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
  150. .serializerByType(Long.class, ToStringSerializer.instance)
  151. .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
  152. .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
  153. .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
  154. .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
  155. .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN));
  156. }
  157. // @Bean
  158. // public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
  159. // MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  160. // ObjectMapper objectMapper = new ObjectMapper();
  161. // // 指定时区
  162. // objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
  163. // // 日期类型字符串处理
  164. // objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));
  165. //
  166. // // Java8日期日期处理
  167. // JavaTimeModule javaTimeModule = new JavaTimeModule();
  168. // javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
  169. // javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
  170. // javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
  171. // javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
  172. // javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
  173. // javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
  174. // objectMapper.registerModule(javaTimeModule);
  175. //
  176. // converter.setObjectMapper(objectMapper);
  177. // return converter;
  178. // }
  179. }

项目中使用(前面两种的合并)

  1. package com.ipebg.base.config;
  2. import cn.hutool.core.date.DateUtil;
  3. import cn.hutool.core.util.StrUtil;
  4. import com.fasterxml.jackson.databind.deser.std.DateDeserializers;
  5. import com.fasterxml.jackson.databind.ser.std.DateSerializer;
  6. import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
  7. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
  8. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
  9. import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
  10. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
  11. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
  12. import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
  13. import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
  14. import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
  15. import org.springframework.context.annotation.Bean;
  16. import org.springframework.context.annotation.Configuration;
  17. import org.springframework.core.convert.converter.Converter;
  18. import java.text.SimpleDateFormat;
  19. import java.time.LocalDate;
  20. import java.time.LocalDateTime;
  21. import java.time.LocalTime;
  22. import java.time.format.DateTimeFormatter;
  23. import java.util.Date;
  24. /**
  25. * 全局日期處理類
  26. *
  27. * @author H2014242
  28. * 2021/3/15 上午 11:19
  29. */
  30. @Configuration
  31. public class DateHandlerConfig {
  32. /**
  33. * DateTime格式化字符串
  34. */
  35. private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
  36. /**
  37. * Date格式化字符串
  38. */
  39. private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
  40. /**
  41. * Time格式化字符串
  42. */
  43. private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";
  44. /**
  45. * LocalDate转换器,用于转换RequestParam和PathVariable参数
  46. */
  47. @Bean
  48. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  49. public Converter<String, LocalDate> localDateConverter() {
  50. return source -> {
  51. if (StrUtil.isEmpty(source)) {
  52. return null;
  53. }
  54. return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
  55. };
  56. }
  57. /**
  58. * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
  59. * 不要听信idea的自动提示将代码转化成lambda方式,会报错
  60. */
  61. @Bean
  62. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  63. public Converter<String, LocalDateTime> localDateTimeConverter() {
  64. return source -> {
  65. if (StrUtil.isEmpty(source)) {
  66. return null;
  67. }
  68. return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
  69. };
  70. }
  71. /**
  72. * LocalDate转换器,用于转换RequestParam和PathVariable参数
  73. */
  74. @Bean
  75. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  76. public Converter<String, LocalTime> localTimeConverter() {
  77. return source -> {
  78. if (StrUtil.isEmpty(source)) {
  79. return null;
  80. }
  81. return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN));
  82. };
  83. }
  84. /**
  85. * Date转换器,用于转换RequestParam和PathVariable参数
  86. */
  87. @Bean
  88. @ConditionalOnBean(name = "requestMappingHandlerAdapter")
  89. public Converter<String, Date> dateConverter() {
  90. return source -> DateUtil.parse(source.trim());
  91. }
  92. /**
  93. * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
  94. */
  95. @Bean
  96. public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
  97. return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
  98. .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
  99. .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
  100. .serializerByType(Long.class, ToStringSerializer.instance)
  101. .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
  102. .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
  103. .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
  104. .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
  105. .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
  106. ;
  107. }
  108. }

总结

controller方法的参数是通过不同的HandlerMethodArgumentResolver完成解析的。如果参数标注了@RequestBody注解,实际上是通过MappingJackson2HttpMessageConverter的ObjectMapper将传入json格式数据反序列化解析成目标类型的。如果标注了@RequestParam注解,是通过在应用初始化时注入到ConversionService的一个个Converter来实现的。其他的HandlerMethodArgumentResolver也是各有各的用处,大家可以再看看相关代码,以便加深理解。

  1. // 只能处理Date类型参数的接受和返回,且必须为下面配置指定的格式。
  2. // 支持 GET请求及POST表单方式请求,支持Content-Type 是application/json的POST请求。
  3. // 不支持 LocalDate等Java8日期API。
  4. spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
  5. spring.jackson.time-zone=GMT+8
  6. // @JsonFormat处理Date类型参数的接受和返回,且必须为下面配置指定的格式。
  7. // @JsonFormat处理LocalDate等Java8日期API,只能返回的json按照指定的格式,如果没有标注@JsonFormat
  8. // 支持 GET请求及POST表单方式请求,支持Content-Type 是application/json的POST请求。
  9. // LocalDate的返回格式为 "2021-04-07",LocalDateTime的返回格式为"2021-04-07T16:53:56.525"
  10. @JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")
  11. private Date releaseDate;
  12. 对于参数而言,org.springframework.core.convert.converter.Converter<S, T>的优先级高于@DateTimeFormat
  13. 也就是定义了对应时间类型的转换器,@DateTimeFormat不再起作用,格式化以转换器为准;
  14. 对于RequestBody和返回结果,@JsonFormat的优先级高于org.springframework.http.converter.HttpMessageConverter<T>,在定义了MappingJackson2HttpMessageConverter对时间的序列化与反序列化的情况下,
  15. 如果Field注解了@JsonFormat且格式不一致,以@JsonFormat注解格式为准

源码剖析

在了解完怎么样进行全局设置后,接下来我们通过debug源码来深入剖析一下Spring MVC是如何进行参数绑定的。

  1. @RequestMapping("/date")
  2. public DateEntity getDate(
  3. LocalDate date,
  4. LocalDateTime dateTime,
  5. Date originalDate,
  6. DateEntity dateEntity) {
  7. System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
  8. return dateEntity;
  9. }

以下是收到请求后的方法调用栈的一些关键方法:

  1. // DispatcherServlet处理请求
  2. doService:943, DispatcherServlet
  3. // 处理请求
  4. doDispatch:1040, DispatcherServlet
  5. // 生成调用链(前处理、实际调用方法、后处理)
  6. handle:87, AbstractHandlerMethodAdapter
  7. handleInternal:793, RequestMappingHandlerAdapter
  8. // 反射获取到实际调用方法,准备开始调用
  9. invokeHandlerMethod:879, RequestMappingHandlerAdapter
  10. invokeAndHandle:105, ServletInvocableHandlerMethod
  11. // 关键步骤,从这里开始处理请求参数
  12. invokeForRequest:134, InvocableHandlerMethod
  13. getMethodArgumentValues:167, InvocableHandlerMethod
  14. resolveArgument:121, HandlerMethodArgumentResolverComposite

下面我们从关键的invokeForRequest:134, InvocableHandlerMethod处开始分析,源码如下

  1. // InvocableHandlerMethod.java
  2. @Nullable
  3. public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  4. Object... providedArgs) throws Exception {
  5. // 这里完成参数的转换,得到的是转换后的值
  6. Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
  7. if (logger.isTraceEnabled()) {
  8. logger.trace("Arguments: " + Arrays.toString(args));
  9. }
  10. // 反射调用,真正开始执行方法
  11. return doInvoke(args);
  12. }
  13. // 具体实现
  14. protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  15. Object... providedArgs) throws Exception {
  16. // 获取当前handler method的方法参数数组,封装了入参信息,比如类型、泛型等
  17. MethodParameter[] parameters = getMethodParameters();
  18. if (ObjectUtils.isEmpty(parameters)) {
  19. return EMPTY_ARGS;
  20. }
  21. // 该数组用来存放从MethodParameter转换后的结果
  22. Object[] args = new Object[parameters.length];
  23. for (int i = 0; i < parameters.length; i++) {
  24. MethodParameter parameter = parameters[i];
  25. parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
  26. args[i] = findProvidedArgument(parameter, providedArgs);
  27. if (args[i] != null) {
  28. continue;
  29. }
  30. // resolvers是定义的成员变量,HandlerMethodArgumentResolverComposite类型,是各式各样的HandlerMethodArgumentResolver的集合。这里来判断一下是否存在支持当前方法参数的参数处理器
  31. if (!this.resolvers.supportsParameter(parameter)) {
  32. throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
  33. }
  34. try {
  35. // 调用HandlerMethodArgumentResolverComposite来处理参数,下面会重点看一下内部的逻辑
  36. args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
  37. }
  38. catch (Exception ex) {
  39. ......
  40. }
  41. }
  42. return args;
  43. }

下面需要进入HandlerMethodArgumentResolverComposite#resolveArgument方法源码里面。

  1. // HandlerMethodArgumentResolverComposite.java
  2. @Override
  3. @Nullable
  4. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  5. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  6. // 这里来获取匹配当前方法参数的参数解析器
  7. HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  8. if (resolver == null) {
  9. throw new IllegalArgumentException("Unsupported parameter type [" +
  10. parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
  11. }
  12. // 调用真正的参数解析器来处理参数并返回
  13. return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  14. }
  15. // 获取匹配当前方法参数的参数解析器
  16. @Nullable
  17. private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
  18. // 首先从缓存中查询是否有适配当前方法参数的参数解析器,首次进入是没有的
  19. HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
  20. if (result == null) {
  21. for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
  22. // 逐个遍历argumentResolvers这个list里的参数解析器来判断是否支持
  23. if (resolver.supportsParameter(parameter)) {
  24. result = resolver;
  25. this.argumentResolverCache.put(parameter, result);
  26. break;
  27. }
  28. }
  29. }
  30. return result;
  31. }

argumentResolvers里一共有26个参数解析器,下面罗列一下常见的。

  1. this.argumentResolvers = {LinkedList@6072} size = 26
  2. 0 = {RequestParamMethodArgumentResolver@6098}
  3. 1 = {RequestParamMapMethodArgumentResolver@6104}
  4. 2 = {PathVariableMethodArgumentResolver@6111}
  5. 3 = {PathVariableMapMethodArgumentResolver@6112}
  6. ......
  7. 7 = {RequestResponseBodyMethodProcessor@6116}
  8. 8 = {RequestPartMethodArgumentResolver@6117}
  9. 9 = {RequestHeaderMethodArgumentResolver@6118}
  10. 10 = {RequestHeaderMapMethodArgumentResolver@6119}
  11. ......
  12. 14 = {RequestAttributeMethodArgumentResolver@6123}
  13. 15 = {ServletRequestMethodArgumentResolver@6124}
  14. ......
  15. 24 = {RequestParamMethodArgumentResolver@6107}
  16. 25 = {ServletModelAttributeMethodProcessor@6133}

所有的参数解析器都实现了HandlerMethodArgumentResolver接口。

  1. public interface HandlerMethodArgumentResolver {
  2. // 上面用到用来判断当前参数解析器是否支持给定的方法参数
  3. boolean supportsParameter(MethodParameter parameter);
  4. // 解析给定的方法参数并返回
  5. @Nullable
  6. Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  7. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
  8. }

到这里我们整理一下思路,对方法参数的解析都是通过逐个遍历找到合适的HandlerMethodArgumentResolver来完成的。比如,如果参数上标注了@RequestParam或者@RequestBody或者@PathVariable注解,SpringMVC会用不同的参数解析器来解析。下面挑一个最常用的RequestParamMethodArgumentResolver来深入分析一下详细的解析流程。

RequestParamMethodArgumentResolver继承自AbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolver实现了HandlerMethodArgumentResolver接口的resolveArgument方法。

  1. // AbstractNamedValueMethodArgumentResolver.java
  2. @Override
  3. @Nullable
  4. public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  5. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  6. // 解析出传入的原始值,作为下面方法的参数
  7. Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
  8. ......
  9. if (binderFactory != null) {
  10. // 创建 DataBinder
  11. WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
  12. try {
  13. // 通过DataBinder进行参数绑定,参数列表:原始值,目标类型,方法参数
  14. arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
  15. }
  16. ......
  17. }
  18. handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
  19. return arg;
  20. }
  21. // DataBinder.java
  22. @Override
  23. @Nullable
  24. public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  25. @Nullable MethodParameter methodParam) throws TypeMismatchException {
  26. // 调用子类的convertIfNecessary方法,这里的具体实现是TypeConverterSupport
  27. return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
  28. }
  29. // TypeConverterSupport.java
  30. @Override
  31. @Nullable
  32. public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  33. @Nullable MethodParameter methodParam) throws TypeMismatchException {
  34. // 调用重载的convertIfNecessary方法,通过MethodParameter构造了类型描述符TypeDescriptor
  35. return convertIfNecessary(value, requiredType,
  36. (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
  37. }
  38. // convertIfNecessary方法
  39. @Nullable
  40. @Override
  41. public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  42. @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
  43. Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
  44. try {
  45. // 调用TypeConverterDelegate的convertIfNecessary方法
  46. return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
  47. }
  48. ......
  49. }

接下来进入TypeConverterDelegate的源码。

  1. // TypeConverterDelegate.java
  2. @Nullable
  3. public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
  4. @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {
  5. // 查找是否有适合需求类型的自定义的PropertyEditor。还记得上面的 使用@ControllerAdvice配合@initBinder 那一节吗,如果有按那样配置,这里就会找到
  6. PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
  7. ConversionFailedException conversionAttemptEx = null;
  8. // 查找到类型转换服务 ConversionService
  9. ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
  10. // 关键判断,如果没有PropertyEditor 就使用ConversionService
  11. if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
  12. TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
  13. if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
  14. try {
  15. // #1,类型转换服务转换完成后就返回,下面会详细解释
  16. return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
  17. }
  18. catch (ConversionFailedException ex) {
  19. // fallback to default conversion logic below
  20. conversionAttemptEx = ex;
  21. }
  22. }
  23. }
  24. Object convertedValue = newValue;
  25. // 关键判断,如果有PropertyEditor就使用PropertyEditor
  26. if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
  27. ......
  28. // 由editor完成转换
  29. convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
  30. }
  31. boolean standardConversion = false;
  32. if (requiredType != null) {
  33. // Try to apply some standard type conversion rules if appropriate.
  34. if (convertedValue != null) {
  35. if (Object.class == requiredType) {
  36. return (T) convertedValue;
  37. }
  38. // 下面是数组、集合类型属性的处理,这里会遍历集合元素,递归调用convertIfNecessary转化,再收集处理结果
  39. else if (requiredType.isArray()) {
  40. // Array required -> apply appropriate conversion of elements.
  41. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {
  42. convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
  43. }
  44. return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
  45. }
  46. else if (convertedValue instanceof Collection) {
  47. // Convert elements to target type, if determined.
  48. convertedValue = convertToTypedCollection(
  49. (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
  50. standardConversion = true;
  51. }
  52. else if (convertedValue instanceof Map) {
  53. // Convert keys and values to respective target type, if determined.
  54. convertedValue = convertToTypedMap(
  55. (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor);
  56. standardConversion = true;
  57. }
  58. if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
  59. convertedValue = Array.get(convertedValue, 0);
  60. standardConversion = true;
  61. }
  62. if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
  63. // We can stringify any primitive value...
  64. return (T) convertedValue.toString();
  65. }
  66. else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {
  67. ......
  68. }
  69. else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {
  70. convertedValue = NumberUtils.convertNumberToTargetClass(
  71. (Number) convertedValue, (Class<Number>) requiredType);
  72. standardConversion = true;
  73. }
  74. }
  75. else {
  76. // convertedValue == null,空值处理
  77. if (requiredType == Optional.class) {
  78. convertedValue = Optional.empty();
  79. }
  80. }
  81. ......
  82. }
  83. // 异常处理
  84. if (conversionAttemptEx != null) {
  85. if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
  86. throw conversionAttemptEx;
  87. }
  88. logger.debug("Original ConversionService attempt failed - ignored since " +
  89. "PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
  90. }
  91. return (T) convertedValue;
  92. }

假如我们配置了自定义的Converter,会进入#1的分支,由ConversionService进行类型转换,以其子类GenericConversionService为例。

  1. // GenericConversionService.java
  2. @Override
  3. @Nullable
  4. public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
  5. ......
  6. // 从缓存中找到匹配类型的conveter,以LocalDateTime为例,会找到我们自定义的localDateTimeConverter
  7. GenericConverter converter = getConverter(sourceType, targetType);
  8. if (converter != null) {
  9. // 通过工具方法调用真正的converter完成类型转换。至此,完成了源类型到目标类型的转换
  10. Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
  11. return handleResult(sourceType, targetType, result);
  12. }
  13. return handleConverterNotFound(source, sourceType, targetType);
  14. }

以上就是处理标注@RequestParam注解的参数的RequestParamMethodArgumentResolver解析流程。
下面来看一下处理标注@RequestBody注解的参数的RequestResponseBodyMethodProcessor的解析流程,仍然是从resolveArgument方法切入。

  1. // RequestResponseBodyMethodProcessor.java
  2. @Override
  3. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  4. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  5. parameter = parameter.nestedIfOptional();
  6. // 在这里完成参数的解析
  7. Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
  8. ......
  9. return adaptArgumentIfNecessary(arg, parameter);
  10. }
  11. @Override
  12. protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
  13. Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
  14. HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
  15. Assert.state(servletRequest != null, "No HttpServletRequest");
  16. ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
  17. // 调用父类AbstractMessageConverterMethodArgumentResolver完成参数解析
  18. Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
  19. if (arg == null && checkRequired(parameter)) {
  20. throw new HttpMessageNotReadableException("Required request body is missing: " +
  21. parameter.getExecutable().toGenericString(), inputMessage);
  22. }
  23. return arg;
  24. }

下面进入父类AbstractMessageConverterMethodArgumentResolver的源码。

  1. // AbstractMessageConverterMethodArgumentResolver.java
  2. @Nullable
  3. protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
  4. Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
  5. ......
  6. EmptyBodyCheckingHttpInputMessage message;
  7. try {
  8. message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
  9. // 遍历HttpMessageConverter
  10. for (HttpMessageConverter<?> converter : this.messageConverters) {
  11. Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
  12. GenericHttpMessageConverter<?> genericConverter =
  13. (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
  14. if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
  15. (targetClass != null && converter.canRead(targetClass, contentType))) {
  16. if (message.hasBody()) {
  17. HttpInputMessage msgToUse =
  18. getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
  19. // 实际由MappingJackson2HttpMessageConverter调用父类AbstractJackson2HttpMessageConverter的read方法完成解析,
  20. body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
  21. ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
  22. body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
  23. }
  24. else {
  25. body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
  26. }
  27. break;
  28. }
  29. }
  30. }
  31. ......
  32. return body;
  33. }
  34. // AbstractJackson2HttpMessageConverter.java
  35. @Override
  36. public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
  37. throws IOException, HttpMessageNotReadableException {
  38. // 获得要转换的目标参数Java类型,如LocalDateTime等
  39. JavaType javaType = getJavaType(type, contextClass);
  40. // 调用本类的readJavaType方法
  41. return readJavaType(javaType, inputMessage);
  42. }
  43. // AbstractJackson2HttpMessageConverter.java
  44. private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
  45. try {
  46. if (inputMessage instanceof MappingJacksonInputMessage) {
  47. Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
  48. if (deserializationView != null) {
  49. return this.objectMapper.readerWithView(deserializationView).forType(javaType).
  50. readValue(inputMessage.getBody());
  51. }
  52. }
  53. // 调用jackson类库,将HTTP的json请求信息解析为需要的参数类型。至此,将json请求转换成目标Java类型
  54. return this.objectMapper.readValue(inputMessage.getBody(), javaType);
  55. }
  56. ......
  57. }