You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将请求头信息传递给自定义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本身无风险,只要确保请求结束后清理资源即可。通过拦截器在请求生命周期内管理时区信息的存储与清理:

  1. 创建时区上下文类:用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();
    }
}
  1. 实现请求拦截器:提取请求头时区信息,存入上下文;请求结束后清空
@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(); // 必须清理,避免线程复用导致脏数据
    }
}
  1. 注册拦截器:让拦截器对所有请求生效
@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("/**");
    }
}
  1. 修改反序列化器方法:从上下文获取时区
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单例反序列化器):

  1. 创建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();
    }
}
  1. 修改拦截器设置时区:
@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;
    }
}
  1. 修改反序列化器,注入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();
    }

    // 其他原有方法不变
}
  1. 更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 14:01:42