Java controller接口出入参时间序列化转换操作
场景:在controller编写的接口,在前后端交互过程中一般都会涉及到时间字段的交互,比如:后端给前端返的数据有时间相关的字段,同样,前端也存在传时间相关的字段给后端,最原始的方式就是前后端都先转换成字符串和时间戳后进行传输,收到后再进行转换,特别麻烦。
为了方便可以使用注解或者配置做到时间字段的自动转换,这里列举两种简单的操作。
方式一、使用注解
就是在日期字段上添加对应的注解(@JsonFormat),例:
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime2;@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime3;
优点:灵活、清晰。
缺点:所有需要转换的字段都需要手动添加、并且只支持时间和指定格式字符串互转,不支持时间戳
方式二、统一配置
一劳永逸的方式,支持时间和 时间戳、字符串 之间的互转
package com.zhh.demo.config;import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.NumberUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;@Configuration
// 表示通过aop框架暴露该代理对象,AopContext能够访问
//@EnableAspectJAutoProxy(exposeProxy = true)
public class ApplicationConfig {/** 年-月-日 时:分:秒 */private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";/*** 序列化 时间格式转换类型* timestamp:时间戳* dateString: 时间字符串格式* */private static final String LOCAL_DATE_TIME_SERIALIZER_TYPE = "dateString";/*** description:适配自定义序列化和反序列化策略*/@Beanpublic Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {return builder -> {// 时间的序列化和反序列化builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer());builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer());// 将long类型数据转化为string给前端 避免前端造成的精度丢失builder.serializerByType(Long.class, ToStringSerializer.instance);builder.serializerByType(BigInteger.class, ToStringSerializer.instance);};}/*** description:序列化* LocalDateTime序列化为毫秒级时间戳*/public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {@Overridepublic void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers)throws IOException {if (value != null) {// 通过配置决定把时间转换成 时间戳 或 时间字符串if ("timestamp".equals(LOCAL_DATE_TIME_SERIALIZER_TYPE)) {// 13位时间戳gen.writeNumber(LocalDateTimeUtil.toEpochMilli(value));} else {// 指定格式的时间字符串gen.writeString(value.format(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));}}}}/*** description:反序列化* 毫秒级时间戳序列化为LocalDateTime*/public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {@Overridepublic LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext)throws IOException {//2023年11月2日: 尝试反序列增加更多的支持, 支持long输入, 支持字符串输入if (p == null) {return null;}String source = p.getText();return parse(source);}}/*** 时间戳字符串或格式化时间字符串转换为 LocalDateTime* 例:1745806578000、2025-04-28 10:16:18* @param source 13位时间戳 或格式化时间字符串* @return*/private static LocalDateTime parse(String source) {// 如果是时间戳if (NumberUtil.isLong(source)) {long timestamp = Long.parseLong(source);if (timestamp > 0) {return LocalDateTimeUtil.of(timestamp, ZoneOffset.of("+8"));} else {return null;}// 如果是格式化时间字符串} else {if (StringUtils.isBlank(source)) {return null;}// 尝试判断能否解析if (canParseByDateTimeFormatter(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) {return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));}return null;}}/*** 判断是否能解析格式化时间字符串* @param source 格式化时间字符串(格式 2025-04-28 10:16:18)* @param dateTimeFormatter 格式 yyyy-MM-dd HH:mm:ss* @return*/private static boolean canParseByDateTimeFormatter(String source, DateTimeFormatter dateTimeFormatter) {try {dateTimeFormatter.parse(source);} catch (DateTimeParseException e) {return false;}return true;}}
测试一下:
在上面配置的基础上添加部分测试使用的代码
用于前后端交互的bean对象:
@Data
public class UserRO {private LocalDateTime createTime1;private LocalDateTime createTime2;@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime3;
}
controller层接口:
@ApiOperation("时间测试")@PostMapping("/time")public UserRO show(@RequestBody UserRO userRO){System.out.println("\n接收到的入参:");System.out.println(userRO.getCreateTime1());System.out.println(userRO.getCreateTime2());System.out.println(userRO.getCreateTime3());System.out.println("\n出参:");LocalDateTime newTime = LocalDateTime.now();UserRO user = new UserRO();user.setCreateTime1(newTime);user.setCreateTime2(newTime);user.setCreateTime3(new Date());System.out.println(user.getCreateTime1());System.out.println(user.getCreateTime2());System.out.println(user.getCreateTime3());System.out.println("\n");return user;}
通过配置类,可以配置后端接口给调用方返的时间格式【字符串、时间戳】、接口调用方传的入参可以是字符串也可以的时间戳,后端会自动解析。