如何在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解

Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着DateSimpleDateFormat进行时间操作。SimpleDateFormat不是线程安全的,而Date处理时间很麻烦,所以Java8提供了LocalDateTimeLocalDateLocalTime等全新的时间操作API。无论是Date还是LocalDate,在开发Spring Boot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat注解来接收前端传值与日期字段绑定,加上@JsonFormat注解来让返回前端的日期字段格式化成我们想要的时间格式。时间和日期类型在开发中使用的频率是非常高的,如果每个字段都加上这两个注解的话是非常繁琐的,有没有一种全局设置的处理方式呢?今天就来向大家介绍一下。

注:本文基于Springboot2.3.0版本。

根据不同的请求方式需要做不同的配置,下文中分为了JSON方式传参和GET请求及POST表单方式传参两种情况。

JSON方式传参

这种情况指的是类型POST,Content-Type 是application/json 方式的请求。对于这类请求,controller中需要加上@RequestBody注解来标注到我们用来接收请求参数的局部变量上,代码如下:

@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {

 public static void main(String[] args) {
  SpringApplication.run(SpringbootDateLearningApplication.class, args);
 }

  /**
  * DateTime格式化字符串
  */
 private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

 /**
  * Date格式化字符串
  */
 private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";

 /**
  * Time格式化字符串
  */
 private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";

 public static class DateEntity {
  private LocalDate date;

  private LocalDateTime dateTime;

  private Date originalDate;

  public LocalDate getDate() {
   return date;
  }

  public void setDate(LocalDate date) {
   this.date = date;
  }

  public LocalDateTime getDateTime() {
   return dateTime;
  }

  public void setDateTime(LocalDateTime dateTime) {
   this.dateTime = dateTime;
  }

  public Date getOriginalDate() {
   return originalDate;
  }

  public void setOriginalDate(Date originalDate) {
   this.originalDate = originalDate;
  }

 }

 @RequestMapping("/date")
 public DateEntity getDate(@RequestBody DateEntity dateEntity) {
  return dateEntity;
 }
} 

假设默认的接收和返回值的格式都是yyyy-MM-dd HH:mm:ss,可以有以下几个方案。

配置application.yml 文件

在application.yml文件中配置上如下内容:

spring:
 jackson:
 date-format: yyyy-MM-dd HH:mm:ss
 time-zone: GMT+8

小结:

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

增加Jackson配置

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

小结:

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

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

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
 ObjectMapper objectMapper = new ObjectMapper();
 // 指定时区
 objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
 // 日期类型字符串处理
 objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));

 // Java8日期日期处理
 JavaTimeModule javaTimeModule = new JavaTimeModule();
 javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
 javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
 javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
 javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
 javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
 javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
 objectMapper.registerModule(javaTimeModule);

 converter.setObjectMapper(objectMapper);
 return converter;
}

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

GET请求及POST表单方式传参

这种方式和上面的JSON方式,在Spring Boot处理的方式是完全不同的。上一种JSON方式传参是在HttpMessgeConverter中通过jackson的ObjectMapper将http请求体转换成我们写在controller中的参数对象的,而这种方式用的是Converter接口(spring-core中定义的用于将源类型(一般是String)转成目标类型的接口),两者是有本质区别的。

自定义参数转换器(Converter)

自定义一个参数转换器,实现上面提到的org.springframework.core.convert.converter.Converter接口,在配置类里配置上以下几个bean,示例如下:

@Bean
public Converter<String, Date> dateConverter() {
 return new Converter<>() {
  @Override
  public Date convert(String source) {
   SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
   try {
    return formatter.parse(source);
   } catch (Exception e) {
    throw new RuntimeException(String.format("Error parsing %s to Date", source));
   }
  }
 };
}

@Bean
public Converter<String, LocalDate> localDateConverter() {
 return new Converter<>() {
  @Override
  public LocalDate convert(String source) {
   return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
  }
 };
}

@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
 return new Converter<>() {
  @Override
  public LocalDateTime convert(String source) {
   return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
  }
 };
}

同时把controller接口增加一些参数,可以发现在接口里单独用变量接收也是可以正常转换的。

@RequestMapping("/date")
public DateEntity getDate(
  LocalDate date,
  LocalDateTime dateTime,
  Date originalDate,
  DateEntity dateEntity) {
 System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
 return dateEntity;
}

小结:

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

使用@DateTimeFormat注解

和前面提到的一样,GET请求及POST表单方式也是可以用@DateTimeFormat来处理的,单独在controller接口参数或者实体类属性中都可以使用,比如@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate。注意,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat注解不生效,两种方式是不兼容的。

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

/**
 * 日期正则表达式
 */
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])";

/**
 * 时间正则表达式
 */
private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";

/**
 * 日期和时间正则表达式
 */
private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;

/**
 * 13位时间戳正则表达式
 */
private static final String TIME_STAMP_REGEX = "1\\d{12}";

/**
 * 年和月正则表达式
 */
private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";

/**
 * 年和月格式
 */
private static final String YEAR_MONTH_PATTERN = "yyyy-MM";

@Bean
public Converter<String, Date> dateConverter() {
 return new Converter<String, Date>() {
  @SuppressWarnings("NullableProblems")
  @Override
  public Date convert(String source) {
   if (StrUtil.isEmpty(source)) {
    return null;
   }
   if (source.matches(TIME_STAMP_REGEX)) {
    return new Date(Long.parseLong(source));
   }
   DateFormat format;
   if (source.matches(DATE_TIME_REGEX)) {
    format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
   } else if (source.matches(DATE_REGEX)) {
    format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
   } else if (source.matches(YEAR_MONTH_REGEX)) {
    format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
   } else {
    throw new IllegalArgumentException();
   }
   try {
    return format.parse(source);
   } catch (ParseException e) {
    throw new RuntimeException(e);
   }
  }
 };
}

小结:

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

使用@ControllerAdvice配合@initBinder

/*
 * 在类上加上@ControllerAdvice
 */
@ControllerAdvice
@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {
 ...
 @InitBinder
 protected void initBinder(WebDataBinder binder) {
  binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
   @Override
   public void setAsText(String text) throws IllegalArgumentException {
    setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
   }
  });
  binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
   @Override
   public void setAsText(String text) throws IllegalArgumentException {
    setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
   }
  });
  binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
   @Override
   public void setAsText(String text) throws IllegalArgumentException {
    setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
   }
  });
  binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
   @Override
   public void setAsText(String text) throws IllegalArgumentException {
    SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
    try {
     setValue(formatter.parse(text));
    } catch (Exception e) {
     throw new RuntimeException(String.format("Error parsing %s to Date", text));
    }
   }
  });
 }
 ...
} 

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

小结:

  • GET请求及POST表单方式请求。
  • 支持LocalDate等Java8日期API。
  • 局部差异化处理

假设按照前面的全局日期格式设置的是:yyyy-MM-dd HH:mm:ss,但是某个Date类型的字段需要特殊处理成yyyy/MM/dd格式来接收或者返回,有以下方案可以选择。

使用@DateTimeFormat@JsonFormat注解

@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date originalDate;

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

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

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

自定义序列化器和反序列化器

/**
 * {@link Date} 序列化器
 */
public class DateJsonSerializer extends JsonSerializer<Date> {
 @Override
 public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws
   IOException {
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  jsonGenerator.writeString(dateFormat.format(date));
 }
}

/**
 * {@link Date} 反序列化器
 */
public class DateJsonDeserializer extends JsonDeserializer<Date> {
 @Override
 public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
  try {
   SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
   return dateFormat.parse(jsonParser.getText());
  } catch (ParseException e) {
   throw new IOException(e);
  }
 }
}

/**
 * 使用方式
 */
@JsonSerialize(using = DateJsonSerializer.class)
@JsonDeserialize(using = DateJsonDeserializer.class)
private Date originalDate;

如上所示,可以在字段上使用@JsonSerialize@JsonDeserialize注解来指定在序列化和反序列化时使用我们自定义的序列化器和反序列化器。

最后再来个兼容JSON方式和GET请求及POST表单方式的完整的配置吧。

@Configuration
public class GlobalDateTimeConfig {

 /**
  * 日期正则表达式
  */
 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])";

 /**
  * 时间正则表达式
  */
 private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";

 /**
  * 日期和时间正则表达式
  */
 private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;

 /**
  * 13位时间戳正则表达式
  */
 private static final String TIME_STAMP_REGEX = "1\\d{12}";

 /**
  * 年和月正则表达式
  */
 private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";

 /**
  * 年和月格式
  */
 private static final String YEAR_MONTH_PATTERN = "yyyy-MM";

 /**
  * DateTime格式化字符串
  */
 private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

 /**
  * Date格式化字符串
  */
 private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";

 /**
  * Time格式化字符串
  */
 private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

 /**
  * LocalDate转换器,用于转换RequestParam和PathVariable参数
  */
 @Bean
 public Converter<String, LocalDate> localDateConverter() {
  return new Converter<String, LocalDate>() {
   @SuppressWarnings("NullableProblems")
   @Override
   public LocalDate convert(String source) {
    if (StringUtils.isEmpty(source)) {
     return null;
    }
    return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
   }
  };
 }

 /**
  * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
  */
 @Bean
 public Converter<String, LocalDateTime> localDateTimeConverter() {
  return new Converter<String, LocalDateTime>() {
   @SuppressWarnings("NullableProblems")
   @Override
   public LocalDateTime convert(String source) {
    if (StringUtils.isEmpty(source)) {
     return null;
    }
    return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
   }
  };
 }

 /**
  * LocalDate转换器,用于转换RequestParam和PathVariable参数
  */
 @Bean
 public Converter<String, LocalTime> localTimeConverter() {
  return new Converter<String, LocalTime>() {
   @SuppressWarnings("NullableProblems")
   @Override
   public LocalTime convert(String source) {
    if (StringUtils.isEmpty(source)) {
     return null;
    }
    return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
   }
  };
 }

 /**
  * Date转换器,用于转换RequestParam和PathVariable参数
  */
 @Bean
 public Converter<String, Date> dateConverter() {
  return new Converter<String, Date>() {
   @SuppressWarnings("NullableProblems")
   @Override
   public Date convert(String source) {
    if (StringUtils.isEmpty(source)) {
     return null;
    }
    if (source.matches(TIME_STAMP_REGEX)) {
     return new Date(Long.parseLong(source));
    }
    DateFormat format;
    if (source.matches(DATE_TIME_REGEX)) {
     format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
    } else if (source.matches(DATE_REGEX)) {
     format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
    } else if (source.matches(YEAR_MONTH_REGEX)) {
     format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
    } else {
     throw new IllegalArgumentException();
    }
    try {
     return format.parse(source);
    } catch (ParseException e) {
     throw new RuntimeException(e);
    }
   }
  };
 }

 /**
  * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
  */
 @Bean
 public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
  return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
    .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
    .serializerByType(Long.class, ToStringSerializer.instance)
    .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
    .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
 }

}

源码剖析

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

仍然是以上面的controller为例进行debug。

@RequestMapping("/date")
public DateEntity getDate(
  LocalDate date,
  LocalDateTime dateTime,
  Date originalDate,
  DateEntity dateEntity) {
 System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
 return dateEntity;
}

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

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

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

// InvocableHandlerMethod.java
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  Object... providedArgs) throws Exception {
 // 这里完成参数的转换,得到的是转换后的值
 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
 if (logger.isTraceEnabled()) {
  logger.trace("Arguments: " + Arrays.toString(args));
 }
 // 反射调用,真正开始执行方法
 return doInvoke(args);
}
// 具体实现
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
  Object... providedArgs) throws Exception {
 // 获取当前handler method的方法参数数组,封装了入参信息,比如类型、泛型等
 MethodParameter[] parameters = getMethodParameters();
 if (ObjectUtils.isEmpty(parameters)) {
  return EMPTY_ARGS;
 }
 // 该数组用来存放从MethodParameter转换后的结果
 Object[] args = new Object[parameters.length];
 for (int i = 0; i < parameters.length; i++) {
  MethodParameter parameter = parameters[i];
  parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
  args[i] = findProvidedArgument(parameter, providedArgs);
  if (args[i] != null) {
   continue;
  }
  // resolvers是定义的成员变量,HandlerMethodArgumentResolverComposite类型,是各式各样的HandlerMethodArgumentResolver的集合。这里来判断一下是否存在支持当前方法参数的参数处理器
  if (!this.resolvers.supportsParameter(parameter)) {
   throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
  }
  try {
   // 调用HandlerMethodArgumentResolverComposite来处理参数,下面会重点看一下内部的逻辑
   args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
  }
  catch (Exception ex) {
 ......
  }
 }
 return args;
}

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

// HandlerMethodArgumentResolverComposite.java
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 // 这里来获取匹配当前方法参数的参数解析器
 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
 if (resolver == null) {
  throw new IllegalArgumentException("Unsupported parameter type [" +
    parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
 }
 // 调用真正的参数解析器来处理参数并返回
 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 获取匹配当前方法参数的参数解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
 // 首先从缓存中查询是否有适配当前方法参数的参数解析器,首次进入是没有的
 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
 if (result == null) {
  for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
   // 逐个遍历argumentResolvers这个list里的参数解析器来判断是否支持
   if (resolver.supportsParameter(parameter)) {
    result = resolver;
    this.argumentResolverCache.put(parameter, result);
    break;
   }
  }
 }
 return result;
}

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

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

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

public interface HandlerMethodArgumentResolver {

 // 上面用到用来判断当前参数解析器是否支持给定的方法参数
 boolean supportsParameter(MethodParameter parameter);

 // 解析给定的方法参数并返回
 @Nullable
 Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
 NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

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

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

// AbstractNamedValueMethodArgumentResolver.java
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

 // 解析出传入的原始值,作为下面方法的参数
 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
 ......
 if (binderFactory != null) {
  // 创建 DataBinder
  WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
  try {
   // 通过DataBinder进行参数绑定,参数列表:原始值,目标类型,方法参数
   arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
  }
 ......
 }

 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

 return arg;
}

// DataBinder.java
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  @Nullable MethodParameter methodParam) throws TypeMismatchException {
 // 调用子类的convertIfNecessary方法,这里的具体实现是TypeConverterSupport
 return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}

// TypeConverterSupport.java
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  @Nullable MethodParameter methodParam) throws TypeMismatchException {
 // 调用重载的convertIfNecessary方法,通过MethodParameter构造了类型描述符TypeDescriptor
 return convertIfNecessary(value, requiredType,
   (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
}
// convertIfNecessary方法
@Nullable
@Override
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
  @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {

 Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
 try {
  // 调用TypeConverterDelegate的convertIfNecessary方法
  return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
 }
 ......
}

接下来进入TypeConverterDelegate的源码。

// TypeConverterDelegate.java
@Nullable
public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
  @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

 // 查找是否有适合需求类型的自定义的PropertyEditor。还记得上面的 使用@ControllerAdvice配合@initBinder 那一节吗,如果有按那样配置,这里就会找到
 PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

 ConversionFailedException conversionAttemptEx = null;

 // 查找到类型转换服务 ConversionService
 ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
 // 关键判断,如果没有PropertyEditor 就使用ConversionService
 if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
  TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
  if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
   try {
    // #1,类型转换服务转换完成后就返回,下面会详细解释
    return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
   }
   catch (ConversionFailedException ex) {
    // fallback to default conversion logic below
    conversionAttemptEx = ex;
   }
  }
 }

 Object convertedValue = newValue;

 // 关键判断,如果有PropertyEditor就使用PropertyEditor
 if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
  ......
  // 由editor完成转换
  convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
 }

 boolean standardConversion = false;

 if (requiredType != null) {
  // Try to apply some standard type conversion rules if appropriate.

  if (convertedValue != null) {
   if (Object.class == requiredType) {
    return (T) convertedValue;
   }
   // 下面是数组、集合类型属性的处理,这里会遍历集合元素,递归调用convertIfNecessary转化,再收集处理结果
   else if (requiredType.isArray()) {
    // Array required -> apply appropriate conversion of elements.
    if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {
     convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
    }
    return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
   }
   else if (convertedValue instanceof Collection) {
    // Convert elements to target type, if determined.
    convertedValue = convertToTypedCollection(
      (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
    standardConversion = true;
   }
   else if (convertedValue instanceof Map) {
    // Convert keys and values to respective target type, if determined.
    convertedValue = convertToTypedMap(
      (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor);
    standardConversion = true;
   }
   if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
    convertedValue = Array.get(convertedValue, 0);
    standardConversion = true;
   }
   if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
    // We can stringify any primitive value...
    return (T) convertedValue.toString();
   }
   else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {
 ......
   }
   else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {
    convertedValue = NumberUtils.convertNumberToTargetClass(
      (Number) convertedValue, (Class<Number>) requiredType);
    standardConversion = true;
   }
  }
  else {
   // convertedValue == null,空值处理
   if (requiredType == Optional.class) {
    convertedValue = Optional.empty();
   }
  }

 ......
 }
 // 异常处理
 if (conversionAttemptEx != null) {
  if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
   throw conversionAttemptEx;
  }
  logger.debug("Original ConversionService attempt failed - ignored since " +
    "PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
 }

 return (T) convertedValue;
}

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

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

以上就是处理标注@RequestParam注解的参数的RequestParamMethodArgumentResolver解析流程。

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

// RequestResponseBodyMethodProcessor.java
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

 parameter = parameter.nestedIfOptional();
 // 在这里完成参数的解析
 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
 ......
 return adaptArgumentIfNecessary(arg, parameter);
}

@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
  Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
 Assert.state(servletRequest != null, "No HttpServletRequest");
 ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
 // 调用父类AbstractMessageConverterMethodArgumentResolver完成参数解析
 Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
 if (arg == null && checkRequired(parameter)) {
  throw new HttpMessageNotReadableException("Required request body is missing: " +
    parameter.getExecutable().toGenericString(), inputMessage);
 }
 return arg;
}

下面进入父类AbstractMessageConverterMethodArgumentResolver的源码。

// AbstractMessageConverterMethodArgumentResolver.java
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
  Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
 ......
 EmptyBodyCheckingHttpInputMessage message;
 try {
  message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
 // 遍历HttpMessageConverter
  for (HttpMessageConverter<?> converter : this.messageConverters) {
   Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
   GenericHttpMessageConverter<?> genericConverter =
     (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
   if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
     (targetClass != null && converter.canRead(targetClass, contentType))) {
    if (message.hasBody()) {
     HttpInputMessage msgToUse =
       getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
     // 实际由MappingJackson2HttpMessageConverter调用父类AbstractJackson2HttpMessageConverter的read方法完成解析,
     body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
       ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
     body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    }
    else {
     body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
    }
    break;
   }
  }
 }
 ......
 return body;
}

// AbstractJackson2HttpMessageConverter.java
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
  throws IOException, HttpMessageNotReadableException {
 // 获得要转换的目标参数Java类型,如LocalDateTime等
 JavaType javaType = getJavaType(type, contextClass);
 // 调用本类的readJavaType方法
 return readJavaType(javaType, inputMessage);
}

// AbstractJackson2HttpMessageConverter.java
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
 try {
  if (inputMessage instanceof MappingJacksonInputMessage) {
   Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
   if (deserializationView != null) {
    return this.objectMapper.readerWithView(deserializationView).forType(javaType).
      readValue(inputMessage.getBody());
   }
  }
  // 调用jackson类库,将HTTP的json请求信息解析为需要的参数类型。至此,将json请求转换成目标Java类型
  return this.objectMapper.readValue(inputMessage.getBody(), javaType);
 }
 ......
}

总结

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

到此这篇关于在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解的文章就介绍到这了,更多相关Spring Boot使用Date和LocalDateTime内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 解决Spring Boot和Feign中使用Java 8时间日期API(LocalDate等)的序列化问题

    LocalDate . LocalTime . LocalDateTime 是Java 8开始提供的时间日期API,主要用来优化Java 8以前对于时间日期的处理操作.然而,我们在使用Spring Boot或使用Spring Cloud Feign的时候,往往会发现使用请求参数或返回结果中有 LocalDate . LocalTime . LocalDateTime 的时候会发生各种问题.本文我们就来说说这种情况下出现的问题,以及如何解决. 问题现象 先来看看症状.比如下面的例子: @Sprin

  • Spring Boot LocalDateTime格式化处理的示例详解

    JDK8的新特性中Time API,其包括Clock.Duration.Instant.LocalDate.LocalTime.LocalDateTime.ZonedDateTime,在这里就不一一介绍了,相信很多人都会使用其代替Date及Calendar来处理日期时间,下面介绍Spring Boot处理LocalDateTime格式. Controller接收LocalDateTime参数 在Spring中,接收LocalDateTime日期时间数据时,只需要使用@DateTimeFormat

  • springboot mybatis里localdatetime序列化问题的解决

    问题起因 主要是使用mybatis作为ORM之后,返回的对象为Map,然后对于数据库的datetime,datestamp类型返回为时间戳而不是标准的时间,这个问题解决方案有两种,大叔分析一下: 1.在mapper的select里,使用mysql这些数据库的函数,dateformat进行转化,缺点,单元测试里使用h2数据库时会找不到这些函数 2.在ObjectMapper反序列化时统一进行处理,这种方式更好,与具体数据库解耦了 实现 >引用依赖包 'org.mybatis:mybatis-typ

  • Spring shiro + bootstrap + jquery.validate 实现登录、注册功能

    之前的文章中我们已经搭建好框架,并且设计好了,数据库. 现在我们开始实现登录功能,这个可以说是Web应用最最最普遍的功能了. 先来说说我们登录的逻辑: 输入用户名.密码(validate进行前端验证)--ajax调用后台action方法--根据用户名调用业务层到数据层查询数据库信息--查询的密码跟用户输入的密码比对--shiro登录身份验证--将用户信息存入session--响应前端--前端跳转 这个是我要告诉大家的姿势,还有很多很多的姿势.下面我们来看具体的代码. 首先前端验证,这里使用了jq

  • 如何在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解

    Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着Date和SimpleDateFormat进行时间操作.SimpleDateFormat不是线程安全的,而Date处理时间很麻烦,所以Java8提供了LocalDateTime.LocalDate和LocalTime等全新的时间操作API.无论是Date还是LocalDate,在开发Spring Boot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat注解来接收前端传值与日期字段绑定,加上@JsonFormat注

  • Spring Boot启动过程(六)之内嵌Tomcat中StandardHost、StandardContext和StandardWrapper的启动教程详解

    StandardEngine[Tomcat].StandardHost[localhost]的启动与StandardEngine不在同一个线程中,它的start: // Start our child containers, if any Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<>(); for (int i = 0; i < childre

  • VirtualBox中最小化安装Centos8.1虚拟机的教程详解

    1.下载相关工具与镜像 下载链接 VirtualBox: https://download.virtualbox.org/virtualbox/6.1.8/VirtualBox-6.1.8-137981-Win.exe Centos8.1: https://mirrors.tuna.tsinghua.edu.cn/centos/8.1.1911/isos/x86_64/CentOS-8.1.1911-x86_64-dvd1.iso 安装VirtualBox 这个与正常软件一样选好位置一路跟着走就

  • spring boot Mybatis 拦截器实现拼接sql和修改的代码详解

    定义一个 SqlIntercepor 类 import com.culturalCenter.placeManage.globalConfig.Interface.InterceptAnnotation; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedSta

  • Spring Boot集成Shiro并利用MongoDB做Session存储的方法详解

    前言 shiro是一个权限框架,具体的使用可以查看其官网 http://shiro.apache.org/ 它提供了很方便的权限认证和登录的功能. 而springboot作为一个开源框架,必然提供了和shiro整合的功能! 之前项目鉴权一直使用的Shiro,那是在Spring MVC里面使用的比较多,而且都是用XML来配置,用Shiro来做权限控制相对比较简单而且成熟,而且我一直都把Shiro的session放在mongodb中,这个比较符合mongodb的设计初衷,而且在分布式项目中mongo

  • Spring Boot利用@Async异步调用:使用Future及定义超时详解

    前言 之前连续写了几篇关于使用@Async实现异步调用的内容,也得到不少童鞋的反馈,其中问题比较多的就是关于返回Future的使用方法以及对异步执行的超时控制,所以这篇就来一起讲讲这两个问题的处理. 如果您对于@Async注解的使用还不了解的话,可以看看之前的文章,具体如下: 使用@Async实现异步调用 使用@Async实现异步调用:自定义线程池 使用@Async实现异步调用:资源优雅关闭 定义异步任务 首先,我们先使用@Async注解来定义一个异步任务,这个方法返回Future类型,具体如下

  • Pycharm中切换pytorch的环境和配置的教程详解

    pytorch安装 注:在训练模型的时候,有时候可能需要不同版本的 torch和torchvision,所以需要配置不同的环境.anconda和pycharm自行安装,接下来在pycharm终端pip安装. 1. torch和torchvision下载 进入pytorch官网,[https://pytorch.org] 进入右下角的网站下载,找到需要的版本,我的版本如下 1.3.0-版本 cp37-python版本3.7 win-Windows系统 2. pycharm终端安装 (1)首先创建一

  • Spring Boot JPA中java 8 的应用实例

    上篇文章中我们讲到了如何在Spring Boot中使用JPA. 本文我们将会讲解如何在Spring Boot JPA中使用java 8 中的新特习惯如:Optional, Stream API 和 CompletableFuture的使用. Optional 我们从数据库中获取的数据有可能是空的,对于这样的情况Java 8 提供了Optional类,用来防止出现空值的情况.我们看下怎么在Repository 中定义一个Optional的方法: public interface BookRepos

  • Spring Boot JPA中使用@Entity和@Table的实现

    本文中我们会讲解如何在Spring Boot JPA中实现class和数据表格的映射. 默认实现 Spring Boot JPA底层是用Hibernate实现的,默认情况下,数据库表格的名字是相应的class名字的首字母大写.命名的定义是通过接口ImplicitNamingStrategy来定义的: /** * Determine the implicit name of an entity's primary table. * * @param source The source inform

  • 如何在Spring Boot中使用MQTT

    为什么选择MQTT MQTT的定义相信很多人都能讲的头头是道,本文章也不讨论什么高大上的东西,旨在用最简单直观的方式让每一位刚接触的同行们可以最快的应用起来 先从使用MQTT需要什么开始分析: 消息服务器 不同应用/设备之间的频繁交互 可能涉及一对多的消息传递 根据上面列举的这三点,我们大概可以了解到, MQTT最适合的场景是消息做为系统的重要组成部分,且参与着系统关键业务逻辑的情形 MQTT, 启动! 既然决定使用它,我们首先要研究的是如何让MQTT正常工作,毕竟它不是简单的在maven里加入

随机推荐