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;
}
需要注意以下几点:
@DateTimeFormat
是Spring的注解,而@JsonFormat
是Jackson的注解,还有timezone = "GMT+8"
需要带上,jackson在序列化时间时是按照国际标准时间GMT进行格式化的,而在国内默认时区使用的是CST时区,两者相差8小时。@DateTimeFormat
的pattern属性值指定的日期时间格式并不是将要转换成的日期格式,这个指定的格式是和传入的参数对应的,假如注解为:@DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss")
,则传入的参数应该是这样的:2018/08/02 22:05:55
,否则会抛出异常。- 用
@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
注解,去掉该注解即可。
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑完美的配置↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
参考资料
非常感谢下面这个博主的文章,收益很大,要想变强,还是得学着看源码,这样就不会被被人写错的东西坑了:)
作者:不敲代码的攻城狮
出处:https://www.cnblogs.com/leigq/
任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人能读懂的代码。