图源:
本文示例基于的最终示例代码修改而来,可以从获取完整示例。
在中我详细说明了如何在Spring Boot项目中处理枚举类型,其中包含在接口的输入和输出阶段处理枚举,除了枚举以外,通常我们还需要处理时间类型,具体来说就是标准类库中的LocalDateTime
或LocalDate
类。
LocalDateTime
和LocalDate
是JDK8引入的时间类,相比Date
和DateTime
,它们本身包含了时区概念,不需要额外处理时区的问题,而且它们的相关格式化处理函数都是线程安全的。所以Java程序中的时间都应该使用这两种类型来处理。
一般的,我们会在在VO和DTO类中将时间相关属性定义为字符串形式,并借助工具函数进行转换,比如:
package cn.icexmoon.books2.book.entity.dto;
// ...
public class CouponDTO {
private Integer addUserId;
private Double amount;
private String expireTime;
private Double enoughAmount;
private CouponType type;
}
package cn.icexmoon.books2.book.service.impl;
// ...
public class CouponServiceImpl implements CouponService {
private CouponMapper couponMapper;
public Coupon getCouponById(int id) {
return couponMapper.getCouponById(id);
}
public int addCoupon(CouponDTO dto) {
Coupon coupon;
switch (dto.getType()) {
case FREE_COUPON:
coupon = new FreeCoupon();
break;
case ENOUGH_COUPON:
coupon = new EnoughCoupon()
.setEnoughAmount(dto.getEnoughAmount());
break;
default:
throw new RuntimeException("不正确的优惠券类型");
}
coupon.setAddTime(LocalDateTime.now())
.setAddUserId(dto.getAddUserId())
.setAmount(dto.getAmount())
.setExpireTime(MyTimeUtil.convert2DateTime(dto.getExpireTime()))
.setType(dto.getType());
couponMapper.addCoupon(coupon);
return coupon.getId();
}
}
因为DTO中时间是字符串,所以这里需要通过工具类转换:
MyTimeUtil.convert2DateTime(dto.getExpireTime())
相应的时间工具函数:
package cn.icexmoon.books2.system.util;
// ...
public class MyTimeUtil {
private static DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// ...
/**
* 将时间字符串转换为LocalDateTime
*
* @param time 时间字符串
* @return
*/
public static LocalDateTime convert2DateTime(String time) {
return LocalDateTime.parse(time, timeFormatter);
}
// ...
}
当然,在Entity类中时间是LocalDateTime
,在持久层MyBatis可以正常处理这种类型的读写,无需我们做额外处理:
package cn.icexmoon.books2.book.entity;
// ...
chain = true)
(public class Coupon {
private Integer id;
private Integer addUserId;
private LocalDateTime addTime;
private LocalDateTime expireTime;
private CouponType type;
private Double amount;
}
@JsonFormat
虽然这样做也没什么太大问题,但需要额外的类型处理依然不是很方便,实际上我们可以借助Jackson在HTTP request body转换为对象时就可以生成正确的时间类型:
package cn.icexmoon.books2.book.entity.dto;
// ...
public class CouponDTO {
private Integer addUserId;
private Double amount;
shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
( private LocalDateTime expireTime;
private Double enoughAmount;
private CouponType type;
}
注解@JsonFormat
可以让时间类型的属性正确从JSON中解析出来或者解析成JSON。
其中shape
属性指定的是JSON中的原始类型,pattern
是时间模式,timezone
是时区。
绝大多数情况原始类型都是String,模式是yyyy-MM-dd HH:mm:ss
,时区是东八区,即:
shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") (
在实际使用中,时间相应的入参都是String,且时区使用默认值即可,所以可以简写为:
pattern = "yyyy-MM-dd HH:mm:ss") (
@DateTimeFormat
对于复杂传参,一般推荐用JSON作为请求报文体传递,不推荐使用查询字符串,因为基于HTTP协议规范,后者有长度限制,且可能被URL编码。但如果通过后者传递时间参数,服务端如何处理?
package cn.icexmoon.books2.book.controller;
// ...
"/book/coupon")
(public class CouponController {
// ...
"/params-add")
( Result addCouponWithParams( Integer addUserId,
Double amount,
LocalDateTime expireTime,
Double enoughAmount,
CouponType type){
CouponDTO dto = new CouponDTO()
.setAddUserId(addUserId)
.setAmount(amount)
.setExpireTime(expireTime)
.setEnoughAmount(enoughAmount)
.setType(type);
return Result.success(couponService.addCoupon(dto));
}
}
这里处理器方法addCouponWithParams
以查询字符串方式接受入参,并且其中有一个时间类型的参数expireTime
。
默认配置下的Spring Boot不能正常进行类型转换,会报错:
org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime';
这个问题可以通过使用@DateTimeFormat
注解来解决,该注解是Spring的一个注解。
package cn.icexmoon.books2.book.controller;
// ...
"/book/coupon")
(public class CouponController {
// ...
"/params-add")
( Result addCouponWithParams( Integer addUserId,
Double amount,
pattern = "yyyy-MM-dd HH:mm:ss")
( LocalDateTime expireTime,
Double enoughAmount,
CouponType type){
// ...
}
}
和@JsonFormat
注解类似,通过pattern
属性为@DateTimeFormat
注解指定一个时间模式即可让框架正确地将查询字符串中的入参转换为时间类型。
入参中还包含枚举类型
CouponType
,这需要一些额外处理,详情可以阅读。
修改默认配置
上面的两种方式相结合,已经可以解决问题,但是需要在项目中添加大量的注解。如果是一个现有项目,可能这样做是合适的,因为修改默认配置可能会引发一些未知的bug。但如果是一个新项目,完全可以通过修改Jackson的默认配置来实现这一点,进而避免添加大量的注解。
可以通过注入一个Jackson2ObjectMapperBuilderCustomizer
类型的JavaBean来修改默认的Jackson的解析行为,在应用启动时,Jackson会加载所有类型为Jackson2ObjectMapperBuilderCustomizer
的Bean,然后通过其customer
方法对用于解析的相关核心组件进行设置。
package cn.icexmoon.books2.system;
// ...
public class MyJacksonConfig {
1)
( public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {
return jacksonObjectMapperBuilder -> {
//针对于Date类型,文本格式化
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
//针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
jacksonObjectMapperBuilder.modules(javaTimeModule);
};
}
}
为了确保自定义的Jackson2ObjectMapperBuilderCustomizer
在系统自动生成的Bean之后注入,这里指定其顺序@Order(1)
(系统自定义的顺序为0)。
这样设置好后就可以正确处理LocalDateTime
类型的JSON解析和编码。
以上这种方式是Spring Boot推荐的在不破坏自动配置机制的情况下修改Jackson编码行为的方式,如果不起作用,可以检查下你搭的应用是否屏蔽了自动配置机制,比如我的示例中就因为在添加
Converter
时采用以下方式引入配置类:public class MyWebAppConfigurer extends WebMvcConfigurationSupport { // ... }导致了自动配置功能被屏蔽,进而导致上边修改Jackson配置的代码不起作用。
上边修改Jackson配置的示例使用了给jacksonObjectMapperBuilder
添加Model
的方式,这是Jackson官方推荐的方式,除此以外,也可以直接按照待处理类型来添加解析器和编码器:
package cn.icexmoon.books2.system;
// ...
public class MyJacksonConfig {
1)
( public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {
return jacksonObjectMapperBuilder -> {
//针对于Date类型,文本格式化
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
//针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
jacksonObjectMapperBuilder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
jacksonObjectMapperBuilder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
};
}
}
这两种方式效果是相同的。
如果想通过
jacksonObjectMapperBuilder
修改Jackson的其它配置,可以参考。
@JsonComponent
除了上边常规方式以外,Spring Boot本身还提供一个注解@JsonComponent
,可以通过这个注解以更简单直观的方式给特定类型加上特殊的JSON编码/解码行为:
package cn.icexmoon.books2.system;
// ...
public class DateTimeJsonComponent {
public static class Serializer extends JsonSerializer<LocalDateTime> {
public void serialize(LocalDateTime localDateTime, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
jgen.writeString(MyTimeUtil.convert2timeStr(localDateTime));
}
}
public static class Deserializer extends JsonDeserializer<LocalDateTime> {
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
return MyTimeUtil.convert2DateTime(jsonParser.getText());
}
}
}
Converter
除了Json字符串以外,还需要修改对普通字符串参数的默认处理,这就需要借助Converter
或PropertyEditor
,这里以添加Converter
举例:
public class Str2LocalDateTimeConverter implements Converter<String, LocalDateTime> {
public LocalDateTime convert(String source) {
return MyTimeUtil.convert2DateTime(source);
}
}
public class MyWebAppConfigurer implements WebMvcConfigurer {
public void addFormatters(FormatterRegistry registry) {
// ...
registry.addConverter(new Str2LocalDateTimeConverter());
}
}
之后就可以直接在Controller中接收LocalDateTime
类型的查询参数了,Spring可以用我们添加的Converter
进行转换:
"/book/coupon")
(public class CouponController {
// ...
"/params-add")
( Result addCouponWithParams( Integer addUserId,
Double amount,
LocalDateTime expireTime,
Double enoughAmount,
CouponType type){
// ...
}
}
这样做是可行的,但不是最方便和合理的,实际上可以利用
java.time.format.DateTimeFormatter
的相关类进行设置,具体可以阅读
谢谢阅读。
最终的完整示例代码可以从获取。
文章评论