fix(jackson): 统一 LocalDateTime 毫秒时间戳编解码,消除 Feign/WebFlux 的 ObjectMapper 时序问题

pull/206/head
egd 2025-08-21 15:54:47 +08:00
parent b30396d149
commit 35d62571fd
3 changed files with 107 additions and 65 deletions

View File

@ -14,34 +14,24 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
@AutoConfiguration @AutoConfiguration(after = JacksonAutoConfiguration.class)
@Slf4j @Slf4j
public class YudaoJacksonAutoConfiguration { public class YudaoJacksonAutoConfiguration {
/**
* JsonUtils使 ObjectMapper
*/
@Bean
@SuppressWarnings("InstantiationOfUtilityClass")
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
JsonUtils.init(objectMapper);
log.info("[init][初始化 JsonUtils 成功]");
return new JsonUtils();
}
/** /**
* Builder 使 *ByType handledType * Builder 使 *ByType handledType
*/ */
@Bean @Bean
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() { public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
return builder -> builder return builder -> builder
// Long -> String // Long -> Number
.serializerByType(Long.class, NumberSerializer.INSTANCE) .serializerByType(Long.class, NumberSerializer.INSTANCE)
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE) .serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
// LocalDate / LocalTime // LocalDate / LocalTime
@ -60,7 +50,7 @@ public class YudaoJacksonAutoConfiguration {
@Bean @Bean
public Module timestampSupportModuleBean() { public Module timestampSupportModuleBean() {
SimpleModule m = new SimpleModule("TimestampSupportModule"); SimpleModule m = new SimpleModule("TimestampSupportModule");
// Long -> String,避免前端精度丢失 // Long -> Number,避免前端精度丢失
m.addSerializer(Long.class, NumberSerializer.INSTANCE); m.addSerializer(Long.class, NumberSerializer.INSTANCE);
m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE); m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE);
// LocalDate / LocalTime // LocalDate / LocalTime
@ -74,4 +64,15 @@ public class YudaoJacksonAutoConfiguration {
return m; return m;
} }
/**
* JsonUtils使 ObjectMapper
*/
@Bean
@SuppressWarnings("InstantiationOfUtilityClass")
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
JsonUtils.init(objectMapper);
log.debug("[init][初始化 JsonUtils 成功]");
return new JsonUtils();
}
} }

View File

@ -0,0 +1,92 @@
package cn.iocoder.yudao.gateway.jackson;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Configuration
@Slf4j
public class GatewayJacksonAutoConfiguration {
/**
* Builder 使 *ByType handledType
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() {
return builder -> builder
// Long -> Number
.serializerByType(Long.class, NumberSerializer.INSTANCE)
.serializerByType(Long.TYPE, NumberSerializer.INSTANCE)
// LocalDate / LocalTime
.serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE)
.deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE)
.serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE)
.deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE)
// LocalDateTime < - > EpochMillis
.serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
}
/**
* Bean ModuleBoot ObjectMapper
*/
@Bean
public Module timestampSupportModuleBean() {
SimpleModule m = new SimpleModule("TimestampSupportModule");
// Long -> Number
m.addSerializer(Long.class, NumberSerializer.INSTANCE);
m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE);
// LocalDate / LocalTime
m.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
m.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
m.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
m.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
// LocalDateTime < - > EpochMillis
m.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE);
m.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
return m;
}
/**
* JsonUtils使 ObjectMapper
*/
@Bean
@SuppressWarnings("InstantiationOfUtilityClass")
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
JsonUtils.init(objectMapper);
log.debug("[init][初始化 JsonUtils 成功]");
return new JsonUtils();
}
/**
* WebFlux 使 ObjectMapper
*/
@Bean
public CodecCustomizer unifyJackson(ObjectMapper om) {
return configurer -> {
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(om);
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(om);
configurer.defaultCodecs().jackson2JsonDecoder(decoder);
configurer.defaultCodecs().jackson2JsonEncoder(encoder);
};
}
}

View File

@ -1,51 +0,0 @@
package cn.iocoder.yudao.gateway.jackson;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.json.databind.NumberSerializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeDeserializer;
import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
@Configuration
@Slf4j
public class JacksonAutoConfiguration {
@Bean
public JsonUtils jsonUtils(List<ObjectMapper> objectMappers) {
// 1.1 创建 SimpleModule 对象
SimpleModule simpleModule = new SimpleModule();
simpleModule
// 新增 Long 类型序列化规则,数值超过 2^53-1在 JS 会出现精度丢失问题,因此 Long 自动序列化为字符串类型
.addSerializer(Long.class, NumberSerializer.INSTANCE)
.addSerializer(Long.TYPE, NumberSerializer.INSTANCE)
.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE)
.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE)
.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE)
.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE)
// 新增 LocalDateTime 序列化、反序列化规则,使用 Long 时间戳
.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE)
.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE);
// 1.2 注册到 objectMapper
objectMappers.forEach(objectMapper -> objectMapper.registerModule(simpleModule));
// 2. 设置 objectMapper 到 JsonUtils
JsonUtils.init(CollUtil.getFirst(objectMappers));
log.info("[init][初始化 JsonUtils 成功]");
return new JsonUtils();
}
}