SpringBoot关于时间入参、出参之处理

局部处理

  • 入参

    入参就是在Controller层的方法参数中使用到了Date、LocalDateTime去接收前端传过来的时间参数,或者你是用对象接收,对象里面有Date、LocalDateTime这样的属性的。

  • 出参

    从后端返回到前端的数据中带Date、LocalDateTime这样的属性的

入参局部处理用 @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")注解,出参用 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") 注解,
向下面这样:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User{
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime localDateTime;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private LocalDate localDate;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
    private LocalTime localTime;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date date;
}

需要注意以下几点:

  1. @DateTimeFormat是Spring的注解,而@JsonFormat是Jackson的注解,还有timezone = "GMT+8"需要带上,jackson在序列化时间时是按照国际标准时间GMT进行格式化的,而在国内默认时区使用的是CST时区,两者相差8小时。
  2. @DateTimeFormat的pattern属性值指定的日期时间格式并不是将要转换成的日期格式,这个指定的格式是和传入的参数对应的,假如注解为:@DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss"),则传入的参数应该是这样的:2018/08/02 22:05:55,否则会抛出异常。
  3. @DateTimeFormat 时,无论是 Date、LocalDateTime、LocalDate、LocalTime都可以用yyyy-MM-dd HH:mm:ss,但是用 @JsonFormat 时就不行,你可以多给框架数据,它可以不要,但是不能少给。

测试Controller:

@RestController
@RequestMapping("/my_test")
public class TestController {

    @GetMapping("/users")
    public User getUser(User user) {
        // 入参测试
        System.out.println(user);
        // 出参测试
        return User.builder()
                .localDateTime(LocalDateTime.now())
                .localDate(LocalDate.now())
                .localTime(LocalTime.now())
                .date(new Date())
                .build();
    }
}

效果如下:

全局处理

入参

处理Date类型的入参很简单,这样配置就行了:

Application.yml

# MVC 入参时间处理,不支持 Java8 时间
spring:  
  mvc:
    date-format: 'yyyy-MM-dd HH:mm:ss'

想要支持 Java8 的 LocalDateTime就可以向下面这样:

直接上代码吧,原理就是使用 @InitBinder 和 @ControllerAdvice 注解实现,在每个Controller方法执行之前先执行 initBinder() 方法,将前端传过来的时间字符串进行转换后再绑定到参数上面。

想了解 @InitBinder 注解的看这里:https://www.cnblogs.com/lvbinbin2yujie/p/10459303.html

GlobalParamsHand.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 全局参数处理
 * <br/>
 * <p>
 * 创建人:LeiGQ <br>
 * 创建时间:2020-07-21 17:09 <br>
 * <p>
 * 修改人: <br>
 * 修改时间: <br>
 * 修改备注: <br>
 * </p>
 */
@Slf4j
@ControllerAdvice
public class GlobalParamsHand {

    /**
     * The type Custom local date time editor.
     *
     * @author leiguoqing
     * @date 2020 -07-21 23:24:50
     */
    private static class CustomLocalDateTimeEditor extends PropertyEditorSupport {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            LocalDateTime localDateTime = LocalDateTime.parse(text, formatter);
            super.setValue(localDateTime);
        }
    }


    /**
     * 后端接收 LocalDateTime 类型的参数,此方法会在每个Controller方法执行前执行,从而达到将前端传来的字符串时间转换为 LocalDateTime
     * <br/>
     * Date 类型转换已在 Application.yml 中使用 Spring.mvc.date-format: 'yyyy-MM-dd HH:mm:ss'配置
     * <br/>
     * 参考:<a href='https://www.jianshu.com/p/cb108ecbec89'>后端接收java.util.Date类型的参数</a>
     *
     * @param binder the binder
     * @author leiguoqing
     * @date 2020 -07-21 22:48:27
     */
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // 注册 CustomLocalDateTimeEditor
        binder.registerCustomEditor(LocalDateTime.class, new CustomLocalDateTimeEditor());
    }
}

出参

SpringBoot默认是使用Jackson来序列化的,我们就需要重新配置Jackson,像下面这样:

我把出参的全局配置配置成LocalDateTime和Date均返回时间戳给前端,前端需要什么格式自己定,同时对科学记数法做了处理,如果你想改为类似 yyyy-MM-dd HH:mm:ss 这样的格式,可以参考这个工具类里面改:https://blog.csdn.net/qq_34845394/article/details/90072563

先来看看下面这个有问题的配置,具体如下:

↓↓↓↓↓↓↓↓↓有问题的配置↓↓↓↓↓↓↓↓↓↓↓↓↓↓

JacksonConfig.java

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.time.LocalDateTime;
import java.util.TimeZone;

/**
 * ObjectMapper配置
 */
@Configuration
public class JacksonConfig {

    @Bean(name = "objMapper")
    @Primary
    public ObjectMapper getObjMapper() {
        ObjectMapper objectMapper = new ObjectMapper();

        // objectMapper.configure() 方法与 objectMapper.disable(), objectMapper.enable() 作用一样,
        // 都是进行一些配置,查看源码得知:都是调用 _serializationConfig.without(f) 方法
        /*禁用一些配置*/
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        /*启用一些配置*/
        // 使科学计数法完整返回给前端
        objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
        //  时间项目推荐返回前端时间戳,前端根据需要自己转换格式
        objectMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        /*时间模块*/
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        /*序列化配置, 针对java8 时间 项目推荐返回前端时间戳,前端根据需要自己转换格式*/
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());

        /*注册模块*/
        objectMapper.registerModule(javaTimeModule)
                .registerModule(new Jdk8Module())
                .registerModule(new ParameterNamesModule());

        // 属性命名策略
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
        // 时区
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));

        // Date 时间格式(非 java8 时间),也统一用时间戳,注释掉
//        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        return objectMapper;
    }

}

LocalDateTimeSerializer.java

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
 * LocalDateTime 序列化 为时间戳
 */
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {

    @Override
    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
        jsonGenerator.writeNumber(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
    }
}

配置好Jackson之后,还需要把这个 objMapper 配置进 Springboot 的消息转换器中,像下面这样:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 使用 JacksonConfig 中的 objMapper,兼容 java8 时间
     *
     * @see JacksonConfig#getObjMapper()
     */
    private final ObjectMapper objMapper;

    // 构造注入
    public MvcConfig(@Qualifier(value = "objMapper") ObjectMapper objMapper) {
        this.objMapper = objMapper;
    }

    /**
     * 配置消息转换器
     * <br>创建人: leigq
     * <br>创建时间: 2018-11-14 16:29
     * <br>
     *
     * @param converters 转换器
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加映射 Jackson2 Http消息转换器
        converters.add(customJackson2HttpMessageConverter());
    }

    /**
     * 映射 Jackson2 Http消息转换器
     * 参考:https://www.cnblogs.com/anemptycup/p/11313481.html
     */
    @Bean
    public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        jsonConverter.setObjectMapper(objMapper);
        return jsonConverter;
    }

}

这样我们返回给前端的属性中有Date或LocalDateTime的就会转成时间戳了。

↑↑↑↑↑↑↑↑↑↑↑↑↑↑有问题的配置↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

为什么有问题???原因如下:

当我们手动配置了ObjectMapper对象后,在application.yml中的jackson配置会失效,原因是我们手动注入了ObjectMapper对象,SpringBoot检测到该对象已经存在,则不会再注入了,所以配置自然不会生效了,具体去看这个类源码就知道了:JacksonHttpMessageConvertersConfiguration

那有没有一种可以让application.yml中的配置生效,我们只做配置增强的方法呢?答案是肯定的。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓完美的配置↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

JacksonConfig.java

import com.blog.www.util.JacksonUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;
import java.util.TimeZone;


/**
 * ObjectMapper配置,在使用 ObjectMapper 的地方,方便直接注入 ObjectMapper 进行使用,
 * 但是推荐统一使用 {@link JacksonUtils} 工具类
 *
 * @author leiguoqing
 * @date 2020 -07-22 21:03:08
 */
@Configuration
public class JacksonConfig {

    /**
     * jackson 2 ObjectMapper 构建定制器
     * <br/>
     * 该段代码并未覆盖SpringBoot自动装配的ObjectMapper对象,而是加强其配置. 详情请参考: <a href='https://www.jianshu.com/p/68fce8b23341'>SpringBoot2.x下的ObjectMapper配置原理</a>
     *
     * @return the jackson 2 object mapper builder customizer
     * @author leiguoqing
     * @date 2020 -07-22 21:23:18
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customJackson() {
        return jacksonObjectMapperBuilder -> {
            //若POJO对象的属性值为null,序列化时不进行显示,暂时注释掉,为空也显示
//            jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL);

            /*禁用一些配置, 已在 application.yml配置*/
//            jacksonObjectMapperBuilder.failOnUnknownProperties(false);

            /* 启用一些配置 */
            // 使科学计数法完整返回给前端,已在 application.yml配置
//            jacksonObjectMapperBuilder.featuresToEnable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);

            // 时间项目推荐返回前端时间戳,前端根据需要自己转换格式, 已在 application.yml配置
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);


            /* 时间模块 */
            JavaTimeModule javaTimeModule = new JavaTimeModule();
            /* 序列化配置, 针对java8 时间 项目推荐返回前端时间戳,前端根据需要自己转换格式*/
            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());

            /* 注册模块 */
            jacksonObjectMapperBuilder.modules(javaTimeModule, new Jdk8Module(), new ParameterNamesModule());

            // 属性命名策略
            jacksonObjectMapperBuilder.propertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);

            // 时区, 已在 application.yml配置
//            jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone("GMT+8"));

            // 针对于Date类型,文本格式化,Date 时间格式(非 java8 时间),也统一用时间戳,注释掉
//            jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");




            //-----------------------------------------------华丽的分割线---------------------------------------------------

            /* ↓↓↓↓超级详细的一些配置↓↓↓↓ */
            //若POJO对象的属性值为null,序列化时不进行显示
//            jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL);
//            //若POJO对象的属性值为"",序列化时不进行显示
//            jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
//            //DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES相当于配置,JSON串含有未知字段时,反序列化依旧可以成功
//            jacksonObjectMapperBuilder.failOnUnknownProperties(false);
//            //序列化时的命名策略——驼峰命名法
//            jacksonObjectMapperBuilder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
//            //针对于Date类型,文本格式化
//            jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
//
//            //针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
//            JavaTimeModule javaTimeModule = new JavaTimeModule();
//            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//            jacksonObjectMapperBuilder.modules(javaTimeModule);
//
////            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
//            //默认关闭,将char[]数组序列化为String类型。若开启后序列化为JSON数组。
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS);
//
//            //默认开启,若map的value为null,则不对map条目进行序列化。(已废弃)。
//            // 推荐使用:jacksonObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL);
//            jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.WRITE_NULL_MAP_VALUES);
//
//            //默认开启,将Date类型序列化为数字时间戳(毫秒表示)。关闭后,序列化为文本表现形式(2019-10-23T01:58:58.308+0000)
//            //若设置时间格式化。那么均输出格式化的时间类型。
//            jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
//            //默认关闭,在类上使用@JsonRootName(value="rootNode")注解时是否可以包裹Root元素。
//            // (https://blog.csdn.net/blueheart20/article/details/52212221)
////            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRAP_ROOT_VALUE);
//            //默认开启:如果一个类没有public的方法或属性时,会导致序列化失败。关闭后,会得到一个空JSON串。
//            jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
//
//            //默认关闭,即以文本(ISO-8601)作为Key,开启后,以时间戳作为Key
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
//
//            //默认禁用,禁用情况下,需考虑WRITE_ENUMS_USING_TO_STRING配置。启用后,ENUM序列化为数字
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_INDEX);
//
//            //仅当WRITE_ENUMS_USING_INDEX为禁用时(默认禁用),该配置生效
//            //默认关闭,枚举类型序列化方式,默认情况下使用Enum.name()。开启后,使用Enum.toString()。注:需重写Enum的toString方法;
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
//
//            //默认开启,空Collection集合类型输出空JSON串。关闭后取消显示。(已过时)
//            // 推荐使用serializationInclusion(JsonInclude.Include.NON_EMPTY);
//            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS);
//
//            //默认关闭,当集合Collection或数组一个元素时返回:"list":["a"]。开启后,"list":"a"
//            //需要注意,和DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY 配套使用,要么都开启,要么都关闭。
////            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
//
//            //默认关闭。打开后BigDecimal序列化为文本。(已弃用),推荐使用JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN配置
////            jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN);
//            //默认关闭,即使用BigDecimal.toString()序列化。开启后,使用BigDecimal.toPlainString序列化,不输出科学计数法的值。
//            jacksonObjectMapperBuilder.featuresToEnable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
//
//            /**
//             * JsonGenerator.Feature的相关参数(JSON生成器)
//             */
//
//            //默认关闭,即序列化Number类型及子类为{"amount1":1.1}。开启后,序列化为String类型,即{"amount1":"1.1"}
//            jacksonObjectMapperBuilder.featuresToEnable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS);
//
//            /******
//             *  反序列化
//             */
//            //默认关闭,当JSON字段为""(EMPTY_STRING)时,解析为普通的POJO对象抛出异常。开启后,该POJO的属性值为null。
//            jacksonObjectMapperBuilder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
//            //默认关闭
////            jacksonObjectMapperBuilder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
//            //默认关闭,若POJO中不含有JSON中的属性,则抛出异常。开启后,不解析该字段,而不会抛出异常。
//            jacksonObjectMapperBuilder.featuresToEnable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        };
    }
}

LocalDateTimeSerializer.java 和上面的保持一样。

application.yml

spring:
  # ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ jackson 配置 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ #
  ## 自定义 WebMvcConfig 之后(添加 @EnableWebMvc 后),原有properties中的jackson配置会失效,所以必须在自定义实现类中再次对 jackson 的配置进行补充,
  ## 详见:com.blog.www.web.config.MvcConfig 中配置的Jackson消息转换器,原因是我们手动注入了ObjectMapper对象,SpringBoot检测到该对象已经存在,则不会再注入了,所以配置自然不会生效了
  ## 配置参考:https://www.cnblogs.com/liaojie970/p/9396334.html
  jackson:
    # 反序列化
    deserialization:
      FAIL_ON_UNKNOWN_PROPERTIES: false
      USE_BIG_DECIMAL_FOR_FLOATS: true
    # 序列化
    serialization:
      # 使科学计数法完整返回给前端
      WRITE_BIGDECIMAL_AS_PLAIN: true
      # 时间项目推荐返回前端时间戳
      WRITE_DATES_AS_TIMESTAMPS: true

    # 日期格式化
    date-format: 'yyyy-MM-dd HH:mm:ss'
    time-zone: 'GMT+8'

像上面那样配置就不会导致application.yml中的配置失效,我们可以看到,我们在application.yml中进行了简单的配置,然后在JacksonConfig.java中对java8的时间做了处理(application.yml貌似不能配置模块,所以只能用JavaConfig代码配置了)。

需要注意的是,如果发现这样配置不生效,那么很有可能是你在哪个位置加了个 @EnableWebMvc 注解,去掉该注解即可。

↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑完美的配置↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

参考资料

非常感谢下面这个博主的文章,收益很大,要想变强,还是得学着看源码,这样就不会被被人写错的东西坑了:)

posted @ 2019-08-21 00:17  leigq  阅读(4962)  评论(0编辑  收藏  举报