如何在 Spring Boot 应用中优雅的使用 Date 和 LocalDateTime
});
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 继承自 AbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolver 实现了 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 bel
ow
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);
评论