springboot(springmvc) 通过用户的时区国际化时间

本文介绍如何在SpringBoot项目中实现时间的国际化处理,确保不同地区的用户看到的是自己所在时区的时间显示。通过自定义Jackson序列化器实现日期时间的转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

springboot(springmvc) 通过用户的时区国际化时间

需求: 系统有各个国家的用户,服务器在国内,不过时间一定要转换为用户当地时间。
实现通过自定义jackson序列号方法:
(1)用户需要有timeZone字段保存用户所在的时区。
(2)前端JS可以获取到当前系统的时区信息,然后每次请求放到header,后台通过拦截器或者过滤器把时区信息放ThreadLocal来用。

Intl.DateTimeFormat().resolvedOptions().timeZone;  //'Asia/Shanghai'

本次实现并没有用到2不过实现逻辑是一样的。
上代码

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class ConvertConfiguration implements WebMvcConfigurer {

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Autowired(required = false)
    private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        ObjectMapper objectMapper = new ObjectMapper();
        //取消时间的转化格式,默认是时间戳,同时需要设置要表现的时间格式
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
        objectMapper.setDateFormat(new TimeZoneDateFormat("yyyy-MM-dd HH:mm:ss"));
        JavaTimeModule javaTimeModule = new JavaTimeModule();   // 默认序列化没有实现,反序列化有实现
        javaTimeModule.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer(DATE_TIME_FORMATTER));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(TIME_FORMATTER));
        javaTimeModule.addDeserializer(LocalDateTime.class,new CustomLocalDateTimeDeserializer(DATE_TIME_FORMATTER));
        objectMapper.registerModule(javaTimeModule);
        // 设置时区
        objectMapper.setTimeZone(TimeZone.getDefault());
        // 设置格式化输出
//        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
        // 设置蛇形格式
        //objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        if(mappingJackson2HttpMessageConverter == null){
            mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
        }
        mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);
        converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
        converters.add(0, mappingJackson2HttpMessageConverter);
    }


    @Autowired
    private JacksonProperties jacksonProperties;

    /**
     * 时区转换
     *
     * @param localDateTime
     * @param originZoneId
     * @param targetZoneId
     * @return
     */
    public static LocalDateTime convertLocalDateTime(LocalDateTime localDateTime, ZoneId originZoneId,
                                                     ZoneId targetZoneId)
    {
        return localDateTime.atZone(originZoneId).withZoneSameInstant(targetZoneId).toLocalDateTime();
    }

    /**
     * LocalDateTime序列化
     */
    public static class CustomLocalDateTimeSerializer extends JsonSerializer<LocalDateTime>
    {
        private DateTimeFormatter formatter;

        public CustomLocalDateTimeSerializer() {
        }
        public CustomLocalDateTimeSerializer(DateTimeFormatter formatter)
        {
            super();
            this.formatter = formatter;
        }

        @Override
        public void serialize(LocalDateTime value, JsonGenerator generator, SerializerProvider provider)
                throws IOException
        {
            JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal();
            if(jwtUser != null){
                generator.writeString(convertLocalDateTime(value, ZoneId.systemDefault(),ZoneId.of(jwtUser.getTimeZone()) )
                        .format(formatter));
            }else{
                generator.writeString(convertLocalDateTime(value, ZoneId.systemDefault(), ZoneId.systemDefault())
                        .format(formatter));
            }

        }

    }
    

    /**
     * LocalDateTime反序列化
     *
     */
    public static class CustomLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime>
    {
        private DateTimeFormatter formatter;

        public CustomLocalDateTimeDeserializer() {
        }

        public CustomLocalDateTimeDeserializer(DateTimeFormatter formatter)
        {
            super();
            this.formatter = formatter;
        }

        @Override
        public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
                throws IOException {

            JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal();
            if(jwtUser != null){
                return convertLocalDateTime(LocalDateTime.parse(parser.getText(), formatter), ZoneId.of(jwtUser.getTimeZone()),
                        ZoneId.systemDefault());
            }else{
                return convertLocalDateTime(LocalDateTime.parse(parser.getText(), formatter), ZoneId.systemDefault(),
                        ZoneId.systemDefault());
            }

        }
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                "classpath:/META-INF/resources/");
        registry.addResourceHandler("/swagger-ui/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
    }
}

JwtUser中保存了用户时区,如果时区是空的就使用系统默认时区来序列化和反序列化的。上面实现了LocalDateTime的返回和传入的时间序列化。不过我们一般都用Date来接收和返回数据,所以要重写DateFormat。

/**
 * 系统默认时区为亚洲/上海时区,只需要在用户登录后 转换到用户所对应的时区就可
 */
@Configuration
public class TimeZoneDateFormat extends SimpleDateFormat {

    public TimeZoneDateFormat() {
        super();
    }

    //默认为当前服务的区域
    private ZoneId zoneId = ZoneId.systemDefault();

    private String pattern;

    public TimeZoneDateFormat(String pattern) {
        super(pattern);
        this.pattern = pattern;
    }

    public TimeZoneDateFormat(String pattern, Locale locale) {
        super(pattern, locale);
        this.pattern = pattern;
    }

    public TimeZoneDateFormat(String pattern, DateFormatSymbols formatSymbols) {
        super(pattern, formatSymbols);
        this.pattern = pattern;
    }

    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal();
        if(jwtUser == null){
            return super.format(date, toAppendTo, pos);
        }
        DateTimeFormatter formatString = DateTimeFormatter.ofPattern(this.pattern);
        return new StringBuffer(formatString.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.of(jwtUser.getTimeZone()))));
    }



    @Override
    public Date parse(String text, ParsePosition pos) {
        Date date =  super.parse(text, pos);
        JwtUser jwtUser = (JwtUser) SecurityUtils.getSubject().getPrincipal();
        if(jwtUser == null){
            return date;
        }
        //把用户的时间转为系统时间
        DateTimeFormatter formatString = DateTimeFormatter.ofPattern(this.pattern);
        LocalDateTime localDateTime = LocalDateTime.parse(text,formatString);
        ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(jwtUser.getTimeZone())).withZoneSameInstant(zoneId);
        Date date1 = Date.from(zonedDateTime.toInstant());
        return date1;
    }
}

java 中使用Date来接收或返回传参时调用的DateFormat方法重写,

objectMapper.setDateFormat(new TimeZoneDateFormat("yyyy-MM-dd HH:mm:ss"));

这行使用了重写的DateFormat可以在format和parse方法加上自己的实现


测试

请求对象

@Data
public class RequestTest {
    private Date requestDate;
    private Date responseDate;
}

contorller

@RequestMapping(value = "/test",method = RequestMethod.POST)
public ApiResponse test(@RequestBody RequestTest requestTest){
    requestTest.setResponseDate(new Date());
    return ApiResponse.success(null,requestTest);
}

目前我的登录用户时区为 Asia/Tokyo 比北京时间多1个小时。

请求示例
RespDate为系统时间,自动转换为用户所在时区的时间
在这里插入图片描述
reqeustDate会在自动转换为系统所在时区的时间
在这里插入图片描述
传入时间 -1 h自动转为北京时间。至此。

<think>我们正在解决SpringBoot中LocalDateTime类型在JSON序列化时出现的异常。根据引用[2]和引用[3]的信息,这个问题通常是由于Jackson无法正确序列化Java8日期时间类型(如LocalDateTime)引起的。解决方案是配置Jackson的ObjectMapper来支持这些类型。引用[2]提供了一个配置类,通过注册JavaTimeModule模块并禁用将日期写为时间戳的配置,从而正确序列化Java8日期时间类型。引用[3]提到,在SpringCloudFeign客户端调用时,LocalDateTime的反序列化可能会出现问题,但同样可以通过配置Jackson来解决。此外,引用[3]还提到可能需要增加相关依赖。因此,我们可以按照以下步骤解决:1.添加必要的依赖(如果尚未添加):对于SpringBoot项目,通常需要添加`jackson-datatype-jsr310`模块,该模块提供了对Java8日期时间类型的支持。在Maven项目中,可以添加以下依赖:```xml<dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency>```2.配置ObjectMapper:创建一个配置类,注册JavaTimeModule并禁用将日期写为时间戳的功能。这样,日期将序列化为ISO-8601格式的字符串(例如:"2024-06-13T10:15:30")。3.如果使用Feign客户端,确保Feign的配置也使用了正确配置的ObjectMapper。下面是一个配置类的示例(如引用[2]所示):```javaimportcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.SerializationFeature;importcom.fasterxml.jackson.datatype.jsr310.JavaTimeModule;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassJacksonConfig{@BeanpublicObjectMapperobjectMapper(){ObjectMapperobjectMapper=newObjectMapper();//注册Java8日期时间模块objectMapper.registerModule(newJavaTimeModule());//禁用将日期写为时间戳的功能,使用ISO-8601格式objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);returnobjectMapper;}}```这样配置后,SpringMVC将使用这个ObjectMapper来序列化和反序列化JSON数据,从而正确处理LocalDateTime等类型。另外,如果项目中使用的是SpringBoot2.x或更高版本,可能已经自动配置了jackson-datatype-jsr310,但是默认可能没有禁用WRITE_DATES_AS_TIMESTAMPS,所以仍然需要配置禁用时间戳格式。如果问题仍然存在,请检查是否在多个地方配置了ObjectMapper导致覆盖,或者是否有其他配置干扰。注意:引用[4]提到了线程安全问题,但这个问题与日期序列化无关,因此可以忽略。总结:通过添加依赖和配置ObjectMapper,可以解决LocalDateTime序列化异常的问题。相关问题:1.如何自定义Jackson的日期格式?2.在SpringBoot中如何全局配置日期格式?3.除了JavaTimeModule,Jackson还需要配置哪些模块来支持其他Java8特性?</think>### Spring Boot 中解决 LocalDateTime 类型 JSON 序列化异常的解决方案 #### 问题原因分析 Spring Boot 在处理 `LocalDateTime` 类型时出现 `HttpMessageConversionException` 异常,是由于 Jackson 序列化器无法正确处理 Java 8 日期时间类型。主要原因为: 1. **缺少日期时间支持模块**:Jackson 默认不支持 `java.time` 包中的类型 2. **序列化配置错误**:未禁用时间戳格式导致类型转换失败 3. **Feign 客户端特殊问题**:微服务间调用时需额外配置(引用[3]) --- #### 完整解决方案 ##### 步骤 1:添加必要依赖 在 `pom.xml` 中添加 Jackson 对 Java 8 日期时间的支持: ```xml <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.15.2</version> </dependency> ``` ##### 步骤 2:创建 Jackson 配置类 新建 `JacksonConfig.java` 配置类(引用[2]): ```java import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 注册Java 8日期时间模块 mapper.registerModule(new JavaTimeModule()); // 禁用时间戳格式,使用ISO-8601标准格式 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } } ``` ##### 步骤 3:Feign 客户端特殊处理(微服务场景) 若涉及 Feign 调用,需添加额外配置(引用[3]): ```java @Bean public Encoder feignEncoder() { return new SpringEncoder(new ObjectFactory<>() { @Override public HttpMessageConverters object() { return new HttpMessageConverters( new MappingJackson2HttpMessageConverter(objectMapper()) ); } }); } ``` ##### 步骤 4:验证结果 序列化后的 `LocalDateTime` 将显示为标准 ISO 格式: ```json { "createTime": "2023-08-15T14:30:00" } ``` --- #### 关键配置说明 1. **`JavaTimeModule`** 提供对 `java.time` 包类型的序列化支持 2. **`WRITE_DATES_AS_TIMESTAMPS`** 禁用后生成可读的 ISO 格式而非时间戳 3. **序列化格式示例** `LocalDateTime.now()` → `"2023-08-15T14:30:00"` --- #### 常见问题排查 1. **日期格式自定义** 在字段添加注解: ```java @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime createTime; ``` 2. **时区问题** 在配置中添加: ```java mapper.setTimeZone(TimeZone.getDefault()); ``` 3. **全局日期格式**(Spring Boot 2.4+) 在 `application.properties` 中添加: ```properties spring.jackson.time-zone=Asia/Shanghai spring.jackson.date-format=yyyy-MM-dd HH:mm:ss ``` > **注意**:单例模式下的线程安全问题需用 `ThreadLocal` 处理(引用[4]),但此问题与日期序列化无关。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tutuxfsh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值