pcp: слой толерантности времени — читать naive(=UTC) и Z во всём флоте
PcpTimeModule (libs/pcp-types-lib): толерантные Jackson-десериализаторы LocalDateTime/OffsetDateTime/Instant — любой потребитель читает оба формата на проводе и нормализует к UTC. Только чтение, формат записи не меняется. Подключение фронтом по флоту: - Spring Boot auto-configuration + AutoConfiguration.imports — Boot-маппер всех 12 сервисов на pcp-types-lib (без правок в сервисах); - Jackson SPI (META-INF/services) — ручные мапперы с findAndRegisterModules(); - точечно: dynamic-plan/JacksonConfig (ручной ObjectMapper); - tgu (WebFlux, не зависит от либы) — локальное зеркало модуля в CamundaJacksonConfig (Jackson-2 путь). Зачем: микросервисы деплоятся независимо; с толерантностью продюсера можно флипать LocalDateTime -> OffsetDateTime (Z) в любом порядке без синхронного деплоя — потребитель не падает на парсинге офсета. Тесты: PcpTimeModuleTest (оба формата -> один UTC-момент + авто-подхват SPI). Доки: DATETIME_UTC.md (раздел 2bis + найденные баги формата), CHECKLIST.md.
This commit is contained in:
+4
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
|
||||
|
||||
|
||||
@Configuration
|
||||
@@ -28,6 +29,9 @@ class JacksonConfig {
|
||||
// )
|
||||
mapper.registerModule(JavaTimeModule())
|
||||
.registerKotlinModule()
|
||||
// Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
|
||||
// Этот маппер создаётся вручную и не подхватывает модуль ни через Boot-бин, ни через SPI.
|
||||
.registerModule(PcpTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
return mapper
|
||||
|
||||
+4
@@ -24,6 +24,10 @@ class CamundaJacksonConfig {
|
||||
return JsonMapper.builder()
|
||||
.addModule(KotlinModule.Builder().build())
|
||||
.addModule(JavaTimeModule())
|
||||
// Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
|
||||
// Покрывает Jackson-2 путь (Camunda/внутренний парсинг). REST-codec на Jackson 3
|
||||
// (WebFlux auto-config) — отдельный tgu-follow-up в DATETIME_UTC.md.
|
||||
.addModule(PcpTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build()
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package space.nstart.pcp_tgu_service.config
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
/**
|
||||
* Локальное зеркало слоя толерантности времени из libs/pcp-types-lib
|
||||
* (`space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`).
|
||||
*
|
||||
* Канонический источник — там; здесь копия, потому что pcp-tgu-service работает на WebFlux,
|
||||
* а pcp-types-lib тянет `spring-boot-starter-web` (MVC/Tomcat) и переключил бы тип приложения
|
||||
* на сервлетный. Поведение обязано совпадать с библиотечным; правки синхронизировать.
|
||||
*
|
||||
* Любой потребитель с этим модулем читает время в обоих форматах на проводе (naive=UTC и `Z`/офсет),
|
||||
* нормализуя к UTC. Трогает только десериализацию. См. docs/standards/DATETIME_UTC.md.
|
||||
*/
|
||||
class PcpTimeModule : SimpleModule(NAME) {
|
||||
|
||||
init {
|
||||
addDeserializer(LocalDateTime::class.java, TolerantLocalDateTimeDeserializer())
|
||||
addDeserializer(OffsetDateTime::class.java, TolerantOffsetDateTimeDeserializer())
|
||||
addDeserializer(Instant::class.java, TolerantInstantDeserializer())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NAME = "PcpTimeModule"
|
||||
}
|
||||
}
|
||||
|
||||
private class TolerantLocalDateTimeDeserializer : JsonDeserializer<LocalDateTime>() {
|
||||
|
||||
private val default = LocalDateTimeDeserializer.INSTANCE
|
||||
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime? {
|
||||
if (p.currentToken() == JsonToken.VALUE_STRING) {
|
||||
val text = p.text.trim()
|
||||
if (text.isEmpty()) return null
|
||||
try {
|
||||
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
||||
} catch (_: DateTimeParseException) {
|
||||
// naive-строка без зоны — трактуем как UTC
|
||||
}
|
||||
}
|
||||
return default.deserialize(p, ctxt)
|
||||
}
|
||||
}
|
||||
|
||||
private class TolerantOffsetDateTimeDeserializer : JsonDeserializer<OffsetDateTime>() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val default = InstantDeserializer.OFFSET_DATE_TIME as JsonDeserializer<OffsetDateTime>
|
||||
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OffsetDateTime? {
|
||||
if (p.currentToken() == JsonToken.VALUE_STRING) {
|
||||
val text = p.text.trim()
|
||||
if (text.isEmpty()) return null
|
||||
try {
|
||||
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC)
|
||||
} catch (_: DateTimeParseException) {
|
||||
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC)
|
||||
}
|
||||
}
|
||||
return default.deserialize(p, ctxt)
|
||||
}
|
||||
}
|
||||
|
||||
private class TolerantInstantDeserializer : JsonDeserializer<Instant>() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val default = InstantDeserializer.INSTANT as JsonDeserializer<Instant>
|
||||
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Instant? {
|
||||
if (p.currentToken() == JsonToken.VALUE_STRING) {
|
||||
val text = p.text.trim()
|
||||
if (text.isEmpty()) return null
|
||||
try {
|
||||
return OffsetDateTime.parse(text).toInstant()
|
||||
} catch (_: DateTimeParseException) {
|
||||
return LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)
|
||||
}
|
||||
}
|
||||
return default.deserialize(p, ctxt)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user