如何将请求头信息传递给自定义Jackson JSON反序列化器
问题
我有一个自定义Jackson JSON反序列化器,用于将用户的本地时间戳转换为UTC时间:
Jackson配置类
@Configuration public class JacksonConfiguration { @Bean public Module javaTimeModule() { JavaTimeModule module = new JavaTimeModule(); module.addDeserializer(LocalDateTime.class, new JsonDatetimeDeserializer()); module.addSerializer(LocalDateTime.class, new JsonDatetimeSerializer()); return module; } }
反序列化器实现
public class JsonDatetimeDeserializer extends JsonDeserializer<LocalDateTime> { @Override public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { String timestamp = jsonParser.getText(); DateTimeFormatConstant dateTimeFormat = findDateTimeFormat(timestamp); LocalDateTime deserializedTime = null; if (dateTimeFormat != null) { switch (dateTimeFormat) { case ISO -> deserializedTime = localTimeToUtc(timestamp, dateTimeFormat.value()); case ISO_WITH_OFFSET -> deserializedTime = zonedTimeWithOffsetToUtc(timestamp, dateTimeFormat.value()); //Add more cases here as more formats are supported and added to DateTimeFormatConstant } } return deserializedTime; } private DateTimeFormatConstant findDateTimeFormat(String timestamp) { DateTimeFormatConstant currentFormat = null; for (DateTimeFormatConstant format : DateTimeFormatConstant.values()) { boolean isValidFormat = validateFormat(format.value(), timestamp); if (isValidFormat) { currentFormat = format; break; } } return currentFormat; } private boolean validateFormat(String format, String time) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); boolean isValid; try { try { LocalDateTime localDateTime = LocalDateTime.parse(time, formatter); localDateTime.format(formatter); isValid = true; } catch (DateTimeException e) { ZonedDateTime zonedDateTime = ZonedDateTime.parse(time, formatter); zonedDateTime.format(formatter); isValid = true; } } catch (DateTimeParseException e) { isValid = false; } return isValid; } private LocalDateTime zonedTimeWithOffsetToUtc(String time, String format) { ZonedDateTime zonedDateTime = ZonedDateTime.parse(time, DateTimeFormatter.ofPattern(format)); ZonedDateTime utcTime = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC); return utcTime.toLocalDateTime(); } private LocalDateTime localTimeToUtc(String time, String format) { AuthenticationDetails authDetails = (AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails(); ZoneId zoneId = authDetails.getZoneId(); LocalDateTime localDateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern(format)); Instant instant = localDateTime.atZone(zoneId).toInstant(); ZonedDateTime zonedDateTimeUTC = ZonedDateTime.ofInstant(instant, ZoneId.of("Etc/UTC")); return zonedDateTimeUTC.toLocalDateTime(); } }
我希望为该方法新增处理逻辑,支持用户通过请求头提供时区偏移信息,但不确定如何将请求头信息从控制器传递到自定义反序列化器。目前考虑过使用ThreadLocal,但担心多请求共用线程时存在风险,求可行解决思路。
可行解决思路
方法1:RequestContextHolder+ThreadLocal(安全实现)
ThreadLocal本身无风险,只要确保请求结束后清理资源即可。通过拦截器在请求生命周期内管理时区信息的存储与清理:
- 创建时区上下文类:用ThreadLocal存储当前请求的时区
public class TimeZoneContext { private static final ThreadLocal<ZoneId> TIME_ZONE_HOLDER = new ThreadLocal<>(); public static void setZoneId(ZoneId zoneId) { TIME_ZONE_HOLDER.set(zoneId); } public static ZoneId getZoneId() { return TIME_ZONE_HOLDER.get(); } public static void clear() { TIME_ZONE_HOLDER.remove(); } }
- 实现请求拦截器:提取请求头时区信息,存入上下文;请求结束后清空
@Component public class TimeZoneInterceptor implements HandlerInterceptor { private static final String TIME_ZONE_HEADER = "X-Time-Zone"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String timeZoneHeader = request.getHeader(TIME_ZONE_HEADER); ZoneId zoneId; if (timeZoneHeader != null && !timeZoneHeader.isEmpty()) { try { zoneId = ZoneId.of(timeZoneHeader); } catch (DateTimeException e) { zoneId = ZoneId.systemDefault(); } } else { // 无请求头时 fallback 到原有认证逻辑 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getDetails() instanceof AuthenticationDetails authDetails) { zoneId = authDetails.getZoneId(); } else { zoneId = ZoneId.systemDefault(); } } TimeZoneContext.setZoneId(zoneId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { TimeZoneContext.clear(); // 必须清理,避免线程复用导致脏数据 } }
- 注册拦截器:让拦截器对所有请求生效
@Configuration public class WebConfig implements WebMvcConfigurer { private final TimeZoneInterceptor timeZoneInterceptor; public WebConfig(TimeZoneInterceptor timeZoneInterceptor) { this.timeZoneInterceptor = timeZoneInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeZoneInterceptor) .addPathPatterns("/**"); } }
- 修改反序列化器方法:从上下文获取时区
private LocalDateTime localTimeToUtc(String time, String format) { ZoneId zoneId = TimeZoneContext.getZoneId(); LocalDateTime localDateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern(format)); Instant instant = localDateTime.atZone(zoneId).toInstant(); ZonedDateTime zonedDateTimeUTC = ZonedDateTime.ofInstant(instant, ZoneId.of("Etc/UTC")); return zonedDateTimeUTC.toLocalDateTime(); }
方法2:直接通过RequestContextHolder获取请求头
无需额外ThreadLocal,直接在反序列化器中从当前请求提取时区信息:
修改反序列化器的deserialize方法:
@Override public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { // 获取当前请求 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String timeZoneHeader = request.getHeader("X-Time-Zone"); ZoneId zoneId; if (timeZoneHeader != null) { try { zoneId = ZoneId.of(timeZoneHeader); } catch (DateTimeException e) { zoneId = ZoneId.systemDefault(); } } else { // fallback 到原有逻辑 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getDetails() instanceof AuthenticationDetails authDetails) { zoneId = authDetails.getZoneId(); } else { zoneId = ZoneId.systemDefault(); } } String timestamp = jsonParser.getText(); DateTimeFormatConstant dateTimeFormat = findDateTimeFormat(timestamp); LocalDateTime deserializedTime = null; if (dateTimeFormat != null) { switch (dateTimeFormat) { case ISO -> deserializedTime = localTimeToUtc(timestamp, dateTimeFormat.value(), zoneId); case ISO_WITH_OFFSET -> deserializedTime = zonedTimeWithOffsetToUtc(timestamp, dateTimeFormat.value()); } } return deserializedTime; } // 调整方法参数,接收传入的zoneId private LocalDateTime localTimeToUtc(String time, String format, ZoneId zoneId) { LocalDateTime localDateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern(format)); Instant instant = localDateTime.atZone(zoneId).toInstant(); ZonedDateTime zonedDateTimeUTC = ZonedDateTime.ofInstant(instant, ZoneId.of("Etc/UTC")); return zonedDateTimeUTC.toLocalDateTime(); }
这种方式实现直接,但每次反序列化都要获取请求对象,性能略低于ThreadLocal方案。
方法3:Spring Request-scoped Bean + Provider
利用Spring的请求作用域Bean存储时区,通过Provider延迟获取(适配Jackson单例反序列化器):
- 创建Request-scoped时区Bean:
@Component @RequestScope public class RequestTimeZone { private ZoneId zoneId; public void setZoneId(ZoneId zoneId) { this.zoneId = zoneId; } public ZoneId getZoneId() { return zoneId != null ? zoneId : ZoneId.systemDefault(); } }
- 修改拦截器设置时区:
@Component public class TimeZoneInterceptor implements HandlerInterceptor { private static final String TIME_ZONE_HEADER = "X-Time-Zone"; private final RequestTimeZone requestTimeZone; public TimeZoneInterceptor(RequestTimeZone requestTimeZone) { this.requestTimeZone = requestTimeZone; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String timeZoneHeader = request.getHeader(TIME_ZONE_HEADER); ZoneId zoneId; if (timeZoneHeader != null && !timeZoneHeader.isEmpty()) { try { zoneId = ZoneId.of(timeZoneHeader); } catch (DateTimeException e) { zoneId = ZoneId.systemDefault(); } } else { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getDetails() instanceof AuthenticationDetails authDetails) { zoneId = authDetails.getZoneId(); } else { zoneId = ZoneId.systemDefault(); } } requestTimeZone.setZoneId(zoneId); return true; } }
- 修改反序列化器,注入Provider:
public class JsonDatetimeDeserializer extends JsonDeserializer<LocalDateTime> { private final Provider<RequestTimeZone> requestTimeZoneProvider; public JsonDatetimeDeserializer(Provider<RequestTimeZone> requestTimeZoneProvider) { this.requestTimeZoneProvider = requestTimeZoneProvider; } @Override public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { ZoneId zoneId = requestTimeZoneProvider.get().getZoneId(); String timestamp = jsonParser.getText(); DateTimeFormatConstant dateTimeFormat = findDateTimeFormat(timestamp); LocalDateTime deserializedTime = null; if (dateTimeFormat != null) { switch (dateTimeFormat) { case ISO -> deserializedTime = localTimeToUtc(timestamp, dateTimeFormat.value(), zoneId); case ISO_WITH_OFFSET -> deserializedTime = zonedTimeWithOffsetToUtc(timestamp, dateTimeFormat.value()); } } return deserializedTime; } private LocalDateTime localTimeToUtc(String time, String format, ZoneId zoneId) { LocalDateTime localDateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern(format)); Instant instant = localDateTime.atZone(zoneId).toInstant(); ZonedDateTime zonedDateTimeUTC = ZonedDateTime.ofInstant(instant, ZoneId.of("Etc/UTC")); return zonedDateTimeUTC.toLocalDateTime(); } // 其他原有方法不变 }
- 更新Jackson配置类:
@Configuration public class JacksonConfiguration { private final Provider<RequestTimeZone> requestTimeZoneProvider; public JacksonConfiguration(Provider<RequestTimeZone> requestTimeZoneProvider) { this.requestTimeZoneProvider = requestTimeZoneProvider; } @Bean public Module javaTimeModule() { JavaTimeModule module = new JavaTimeModule(); module.addDeserializer(LocalDateTime.class, new JsonDatetimeDeserializer(requestTimeZoneProvider)); module.addSerializer(LocalDateTime.class, new JsonDatetimeSerializer()); return module; } }
这种方式完全依托Spring的作用域管理,无需手动清理资源,符合Spring生态,但实现复杂度稍高。
内容的提问来源于stack exchange,提问作者Jacob Massotto
相关产品推荐
相关产品推荐

