如何在Spring Boot应用中优雅的使用Date和LocalDateTime

}

复制代码

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

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 {

@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 {

@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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

总结

互联网大厂比较喜欢的人才特点:对技术有热情,强硬的技术基础实力;主动,善于团队协作,善于总结思考。无论是哪家公司,都很重视高并发高可用技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

**另外本人还整理收藏了2021年多家公司面试知识点以及各种技术点整理 **

下面有部分截图希望能对大家有所帮助。

在这里插入图片描述

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-N388QmMN-1712721513634)]
[外链图片转存中…(img-kJ7UMVIZ-1712721513634)]
[外链图片转存中…(img-UFExduh9-1712721513634)]
[外链图片转存中…(img-1edY7N2S-1712721513635)]
[外链图片转存中…(img-HBuMAsTv-1712721513635)]
[外链图片转存中…(img-u0ZlFFNH-1712721513635)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-B5FKPKMZ-1712721513635)]

总结

互联网大厂比较喜欢的人才特点:对技术有热情,强硬的技术基础实力;主动,善于团队协作,善于总结思考。无论是哪家公司,都很重视高并发高可用技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

**另外本人还整理收藏了2021年多家公司面试知识点以及各种技术点整理 **

下面有部分截图希望能对大家有所帮助。

[外链图片转存中…(img-z04U6ree-1712721513636)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-FP6xhIeJ-1712721513636)]

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值