我们知道jackson的序列化 反序列化 配置方式较多,本文介绍常见的几种。
当然,本文重点难点是在于文章目录中 最后一种配置 通过JsonDeserializer方式 导致@JsonFormat失效的问题。
避开问题的方式有很多 一条路行不通就换一条路,但总会有想解决问题的人 偏要一条路走到黑 ,一个程序员最无助 无非遇到bug 百度谷歌Stack Overflow都搜不到答案的时候, 希望本文可以帮助到有需要的同学。
比如LocalDate字段 只截取年月
@JsonFormat(pattern = "yyyy-MM")
private LocalDate month;
这种方式并不灵活 也不易排查,而且会因为项目中实现了某接口导致某些配置失效的问题,就不做描述了 了解有这种方式 且知道这种方式会有坑就行了。
一个正常比较符合常理的设计 全局配置是不应该覆盖注解的,
因为我们很可能出现特定场景用特定的格式的情况,而这种配置 正好可以满足我们的需求
很不巧,博主项目中不是用的这种方式 所以才有了这篇文章
@Configuration
public class LocalDateTimeSerializerConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
@Bean
public LocalDateTimeSerializer localDateTimeDeserializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer());
}
}
这种方式 LocalDateTime全局时间格式是yyyy-MM-dd HH:mm:ss,
如果某个实体类中 需要特定格式 那就可以配合注解使用,
@JsonFormat注解在这种配置下 优先级仍比较高,会以@JsonFormat注解标注的时间格式为主。
如: @JsonFormat(pattern = “yyyy-MM-dd HH:mm”)
这种方式会导致@JsonFormat失效 它优先级不再是最高
博主在网上并没找到如何解决该问题的提问与回答,但博主比较固执 比较想解决这个问题 经过多次调试后 终于写出了解决方案
这种配置方式 有很多变种写法 , 首先是网上比较常见的 也是最简单的
它们本质都是一样的:继承JsonDeserializer
@Configuration
public class LocalDateTimeSerializerConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
@Bean
@Primary
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public class LocalDateTimeSerializer extends JsonSerializer<localdatetime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.format(ofPattern(pattern)));
}
}
public class LocalDateTimeDeserializer extends JsonDeserializer<localdatetime> {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
return LocalDateTime.parse(p.getValueAsString(), ofPattern(pattern));
}
}
}
还可以使用@JsonComponent注解,和上面的类似 就不重复了。
再介绍一种博主目前项目中使用的方式 直接把工具类代码贴上(会有其它代码):
public class JacksonUtils {
private static final DateTimeFormatter DATETIME_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");
private JacksonUtils() {
}
public final static ObjectMapper MAPPER;
static {
MAPPER = serializingObjectMapper();
}
public static String serialize(Object obj) {
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
public static Object deserialize(String jsonText, TypeReference type) {
try {
return MAPPER.readValue(jsonText, type);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static <T> T deserialize(String jsonText, Class<T> beanClass) {
try {
return MAPPER.readValue(jsonText, beanClass);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static JsonNode deserialize(String jsonText) {
try {
return MAPPER.readTree(jsonText);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static JsonNode toJsonNode(Object obj) {
try {
return MAPPER.readTree(serialize(obj));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static ObjectNode createNode() {
try {
return MAPPER.createObjectNode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static ArrayNode createArrayNode() {
try {
return MAPPER.createArrayNode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Map toMap(Object objEntity) {
if (Objects.isNull(objEntity)) {
return null;
}
Map map = new HashMap();
try {
map = MAPPER.readValue(MAPPER.writeValueAsString(objEntity), Map.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return map;
}
/**
* jackson2 json序列化 null字段输出为空串
*/
public static ObjectMapper serializingObjectMapper() {
//设置日期格式
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//序列化日期格式
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer());
javaTimeModule.addSerializer(Date.class, new DateSerializer(false, simpleDateFormat));
//反序列化日期格式
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer());
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer());
javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String date = jsonParser.getText();
try {
return simpleDateFormat.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
});
objectMapper.registerModule(javaTimeModule)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
//序列化成json时,将所有的Long变成string,以解决js中的精度丢失。
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
//忽略不存在的字段
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
/**
* LocalDateTime序列化
*/
private static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.format(DATETIME_FORMATTER));
}
}
/**
* LocalDateTime反序列化
*/
private static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.parse(p.getValueAsString(), DATETIME_FORMATTER);
}
}
/**
* LocalDate序列化
*/
private static class LocalDateSerializer extends JsonSerializer<LocalDate> {
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 获取value来源的类
Class<?> aClass = gen.getCurrentValue().getClass();
// 获取字段名
String currentName = gen.getOutputContext().getCurrentName();
try {
// 获取字段
Field declaredField = aClass.getDeclaredField(currentName);
// 校验是否LocalDate属性的字段
if (Objects.equals(declaredField.getType(), LocalDate.class)) {
// 是否被@JsonFormat修饰
boolean annotationPresent = declaredField.isAnnotationPresent(JsonFormat.class);
if (annotationPresent) {
String pattern = declaredField.getAnnotation(JsonFormat.class).pattern();
if (StringUtils.isNotEmpty(pattern)) {
gen.writeString(value.format(DateTimeFormatter.ofPattern(pattern)));
} else {
gen.writeString(value.format(DATE_FORMATTER));
}
} else {
gen.writeString(value.format(DATE_FORMATTER));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* LocalDate反序列化
*/
private static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDate.parse(p.getValueAsString(), DATE_FORMATTER);
}
}
/**
* LocalTime序列化
*/
private static class LocalTimeSerializer extends JsonSerializer<LocalTime> {
@Override
public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.format(TIME_FORMATTER));
}
}
/**
* LocalTime反序列化
*/
private static class LocalTimeDeserializer extends JsonDeserializer<LocalTime> {
@Override
public LocalTime deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
return LocalTime.parse(p.getValueAsString(), TIME_FORMATTER);
}
}
}
可以看到 上面是没有使用spring的@Configuration注解的 那么是如何实现注册的呢?
实现WebMvcConfigurer 接口
public class SpringMvcBaseConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converters.add(jackson2HttpMessageConverter());
}
/**
* 时间格式转换器,将Date类型统一转换为yyyy-MM-dd HH:mm:ss格式的字符串
* 长整型转换成String
* 忽略value为null时key的输出
*/
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = JacksonUtils.serializingObjectMapper();
converter.setObjectMapper(objectMapper);
return converter;
}
}
我们解决方案的代码 就是这一段了:
逻辑为:
/**
* LocalDate序列化
*/
private static class LocalDateSerializer extends JsonSerializer<LocalDate> {
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 获取value来源的类
Class<?> aClass = gen.getCurrentValue().getClass();
// 获取字段名
String currentName = gen.getOutputContext().getCurrentName();
try {
// 获取字段
Field declaredField = aClass.getDeclaredField(currentName);
// 校验是否LocalDate属性的字段
//这个判断其实也可以不用 进入该方法中的字段 都是LocalDate类型的
//当然 因为反射时可以取任何字段 如写成了getDeclaredFields()
// 错误或恶意取到了其它字段 就会导致报错 多重校验也是没问题的
if (Objects.equals(declaredField.getType(), LocalDate.class)) {
// 是否被@JsonFormat修饰
boolean annotationPresent = declaredField.isAnnotationPresent(JsonFormat.class);
if (annotationPresent) {
String pattern = declaredField.getAnnotation(JsonFormat.class).pattern();
if (StringUtils.isNotEmpty(pattern)) {
gen.writeString(value.format(DateTimeFormatter.ofPattern(pattern)));
} else {
gen.writeString(value.format(DATE_FORMATTER));
}
} else {
gen.writeString(value.format(DATE_FORMATTER));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
网上说了一大堆乱七八糟的,很误导人。
我们一般配置了jackson的序列化/反序列化之后,项目就不会出现问题了,直到有一天,博主意外发现前端传入"2023-05-27 00:00:00" 时,直接因为格式无法正确解析成localDateTime被spring拦截了,反复思考后 想起一般我们都是使用POST方法进行新增/分页查询等操作,但是被拦截的这个项目 使用的是GET方法进行分页请求,那么是不是和这个有关呢?
果不其然,在localDateTime字段加上DateTimeFormat注解后,能够成功解析。 DateTimeFormat的使用场景: GET请求 (url后面拼接参数时)