Springboot 实现NULL值序列化时按类型给初始值(字符串NULL给空串, List NULL 给[], Map空给{})

1,956 阅读6分钟

背景

null值给初始值, 这是在和app客户端童鞋协作时, 被希望能够实现的功能. 单纯实现这个功能, 网上倒是有很多参考. 但是, 我在实际使用中, 和 LocalDateTime 格式化时. 可能涉及到的配置比较深. 不知道那地方扰乱了springboot的配置. 导致不是null值初始化功能没有生效, 就是LocalDateTime格式化没有生效. 调了一晚上, 终于好像可以了.

NULL值序列化时按类型给初始值<#1>

这应该是客户端童鞋为了方便反序列化(尤其是flutter环境).

思路时, 配置自定义的 MappingJackson2HttpMessageConverter. 其中 ObjectMapper(json序列化Jackson库)需要配置进自定义的BeanSerializerModifier. null值初始化逻辑在这个自定义BeanSerializerModifier.

LocalDateTime 格式化<#2>

springboot 默认状态下, LocalDateTime 并不是大家喜闻乐见的 yyyy-MM-dd HH:mm:ss 的格式. 所以需要有个全局配置. 这部分, 通常配置 ObjectMapper 就可以了.

解决方案

当 #1 和 #2 要同时实现时, 貌似问题多多. 这里就不描述问题了, 直接给出我调出来的可行的配置. ()有点繁琐, 希望遇到更简洁有效的配置)

#1 和 #2 都需要用到 ObjectMapper , 我的思路时, 配置全局的增强的 ObjectMapper , 在两个功能中公用.

GlobalDateTimeConfig

暴露出有关各类日期的json反序列化 Converter.

package com.gx.app.config;

import cn.hutool.core.date.DateUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * https://juejin.im/post/6844904177479450632
 */
@Configuration
public class GlobalDateTimeConfig {

    /**
     * 日期正则表达式
     */
    private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";

    /**
     * 时间正则表达式
     */
    private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";

    /**
     * 日期和时间正则表达式
     */
    private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;

    /**
     * 13位时间戳正则表达式
     */
    private static final String TIME_STAMP_REGEX = "1\\d{12}";

    /**
     * 年和月正则表达式
     */
    private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";

    /**
     * 年和月格式
     */
    private static final String YEAR_MONTH_PATTERN = "yyyy-MM";

    /**
     * DateTime格式化字符串
     */
    private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    /**
     * Date格式化字符串
     */
    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";

    /**
     * Time格式化字符串
     */
    private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<String, LocalDate>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalDate convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
            }
        };
    }

    /**
     * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<String, LocalDateTime>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalDateTime convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
            }
        };
    }

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalTime> localTimeConverter() {
        return new Converter<String, LocalTime>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public LocalTime convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
            }
        };
    }

    /**
     * Date转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, Date> dateConverter() {
        return new Converter<String, Date>() {
            @SuppressWarnings("NullableProblems")
            @Override
            public Date convert(String source) {
                if (StringUtils.isEmpty(source)) {
                    return null;
                }
                if (source.matches(TIME_STAMP_REGEX)) {
                    return new Date(Long.parseLong(source));
                }

                return DateUtil.parse(source);

                /*DateFormat format;
                if (source.matches(DATE_TIME_REGEX)) {
                    format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
                } else if (source.matches(DATE_REGEX)) {
                    format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
                } else if (source.matches(YEAR_MONTH_REGEX)) {
                    format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
                } else {
                    throw new IllegalArgumentException();
                }
                try {
                    return format.parse(source);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }*/
            }
        };
    }

    // /**
    //  * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
    //  */
    // @Bean
    // public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    //     return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    //             .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
    //             .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
    //             .serializerByType(Long.class, ToStringSerializer.instance)
    //             .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
    //             .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
    //             .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
    // }



}


JacksonHttpMessageConverter

自定义的MappingJackson2HttpMessageConverter.

/**
 * 自定义序列化的NULL值处理逻辑: 统一各自类型的初始值
 *
 * ! 基本数据类型会自动初始化. 但 char 类型 '\u0000'. 故, 序列化bean全部用包装类型 !
 *
 * 使用官方自带的json格式类库,fastjson因为content type问题时不时控制台报错、无法直接返回二进制等问题
 *
 * see: https://blog.csdn.net/qq_38132283/article/details/89339817
 * @date 2020年9月8日
 */
public class JacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {
   public JacksonHttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper);
    }
}

ObjectMapperConfig

全局自定义增强的 ObjectMapper. 小技巧时, 在spring默认的ObjectMapper基础上增强配置, 好处是可以保留一些常用默认配置.

package com.gx.app.config;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * 不同于一般配置, 我这里拿到spring里默认配置的ObjectMapper, 在其基础上增强配置.
 *
 * see: https://blog.csdn.net/qq_38132283/article/details/89339817
 * @author dafei
 * @version 0.1
 * @date 2020/9/8 14:24
 */
@Slf4j
@Configuration
public class ObjectMapperConfig {
    @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
    private String pattern;

    @Autowired
    private ObjectMapper objectMapper;

    @PostConstruct
    public void configObjectMapper() {
        // 注入 MyBeanSerializerModifier , 实现各类型NULL值变初始化值
        SerializerFactory serializerFactory = objectMapper.getSerializerFactory().withSerializerModifier(new MyBeanSerializerModifier());
        objectMapper.setSerializerFactory(serializerFactory); // !! 上面代码是生成新的实例了, 所以, 这里要重新set才有效

        // LocalDateTime 相关配置  Date 类型用的默认, 但需要配置文件里配置格式和时区 spring.jackson.date-format
        objectMapper.registerModule(new Jdk8Module());
        JavaTimeModule module = new JavaTimeModule();
        // module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern)));
        module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern)));
        module.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN))); // "yyyy-MM-dd"
        module.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN))); // "HH:mm:ss"
        objectMapper.registerModule(module);
        // 拒绝 Date 序列化成 时间戳
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }


    // @Bean
    // @Primary
    // public ObjectMapper objectMapper() {
    //     ObjectMapper objectMapper = new ObjectMapper();
    //
    //     // 拒绝 Date 序列化成 时间戳
    //     // objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    //     // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    //     // objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    //
    //     // Long 转string, 防止某些前端环境溢出, 如js超过18,19位会溢出
    //     SimpleModule simpleModule = new SimpleModule();
    //     simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
    //     simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
    //     objectMapper.registerModule(simpleModule);
    //
    //     objectMapper.registerModule(new Jdk8Module());
    //     JavaTimeModule module = new JavaTimeModule();
    //     // module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern)));
    //     module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern)));
    //     module.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    //     module.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
    //     objectMapper.registerModule(module);
    //     // 拒绝 Date 序列化成 时间戳
    //     objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    //     // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    //     objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    //
    //
    //     return objectMapper;
    // }



    /**
     * 处理数组类型的null值: []
     */
    public static class NullArrayJsonSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
            if (value == null) {
                jgen.writeStartArray();
                jgen.writeEndArray();
            }
        }
    }

    /**
     * 处理 json object 类型的null值: {}
     * Map,POJO
     */
    public static class NullObjectJsonSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
            if (value == null) {
                jgen.writeStartObject();
                jgen.writeEndObject();
            }
        }
    }


    /**
     * 处理字符串类型的null值: ""
     */
    public static class NullStringJsonSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
            jsonGenerator.writeString(StringUtils.EMPTY);
        }
    }

    /**
     * 处理数字类型的null值: 0
     */
    public static class NullNumberJsonSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
            jsonGenerator.writeNumber(0);
        }
    }

    /**
     * 处理布尔类型的null值: false
     */
    public static class NullBooleanJsonSerializer extends JsonSerializer<Object> {

        @Override
        public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
            jsonGenerator.writeBoolean(false);
        }
    }


    public static class MyBeanSerializerModifier extends BeanSerializerModifier {

        public static final JsonSerializer<Object> NullStringJsonSerializer = new NullStringJsonSerializer();
        public static final JsonSerializer<Object> NullNumberJsonSerializer = new NullNumberJsonSerializer();
        public static final JsonSerializer<Object> NullArrayJsonSerializer = new NullArrayJsonSerializer();
        public static final JsonSerializer<Object> NullBooleanJsonSerializer = new NullBooleanJsonSerializer();
        public static final JsonSerializer<Object> NullObjectJsonSerializer = new NullObjectJsonSerializer();


        @Override
        public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
            //循环所有的beanPropertyWriter
            for (Object beanProperty : beanProperties) {
                BeanPropertyWriter writer = (BeanPropertyWriter) beanProperty;
                //判断字段的类型,如果是array,list,set则注册nullSerializer
                if (isStringType(writer)) {
                    writer.assignNullSerializer(NullStringJsonSerializer);
                } else if (isNumberType(writer)) {
                    writer.assignNullSerializer(NullNumberJsonSerializer);
                } else if (isArrayType(writer)) {
                    // 给writer注册一个自己的nullSerializer
                    writer.assignNullSerializer(NullArrayJsonSerializer);
                } else if (isBooleanType(writer)) {
                    writer.assignNullSerializer(NullBooleanJsonSerializer);
                } else if (isJsonObjectType(writer)) {
                    writer.assignNullSerializer(NullObjectJsonSerializer);
                } else {
                    // // 其他 ""
                    // writer.assignNullSerializer(NullStringJsonSerializer);
                }
            }
            return beanProperties;
        }

        /**
         * 是否是数组
         */
        private boolean isArrayType(BeanPropertyWriter writer) {
            Class<?> clazz = writer.getType().getRawClass();
            return clazz.isArray() || Collection.class.isAssignableFrom(clazz);
        }

        /**
         * 是否是 json object , Map, POJO
         */
        private boolean isJsonObjectType(BeanPropertyWriter writer) {
            Class<?> clazz = writer.getType().getRawClass();
            return Map.class.isAssignableFrom(clazz) || BeanUtil.isBean(clazz);
        }

        /**
         * 是否是string
         */
        private boolean isStringType(BeanPropertyWriter writer) {
            Class<?> clazz = writer.getType().getRawClass();
            return CharSequence.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz);
        }


        /**
         * 是否是int
         */
        private boolean isNumberType(BeanPropertyWriter writer) {
            Class<?> clazz = writer.getType().getRawClass();
            return Number.class.isAssignableFrom(clazz);
        }

        /**
         * 是否是boolean
         */
        private boolean isBooleanType(BeanPropertyWriter writer) {
            Class<?> clazz = writer.getType().getRawClass();
            return clazz.equals(Boolean.class);
        }

    }
}

ObjectMapperConfig

继承WebMvcConfigurationSupport, 应用上面的配置.

package com.gx.app.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gx.common.components.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;
import java.util.Map;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        // diy转换器, 序列化时对 bean properties null 值返回对应类型的初始值
        JacksonHttpMessageConverter jhmc = new JacksonHttpMessageConverter(objectMapper); // 用我自己定制的
        converters.add(jhmc);
        // 追加默认转换器
        super.addDefaultHttpMessageConverters(converters);
    }

    // 注册自定义的 Converter
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 拿到配置所有 Converter 注册进去  GlobalDateTimeConfig
        Map<String, Converter> converterMap = SpringContextUtils.getApplicationContext().getBeansOfType(Converter.class);
        converterMap.forEach((k, v) -> {
            registry.addConverter(v);
            log.info("==> added converter: {}", k);
        });
    }

}

优化: 实现 WebMvcConfigurer 接口方式, 不会破坏静态资源配置

package com.gx.app.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gx.common.components.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;
import java.util.Map;

/**
 * 实现 WebMvcConfigurer 接口方式, 不会破坏静态资源配置. see: https://blog.csdn.net/qq_41953685/article/details/90415166
 */
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer { // extends WebMvcConfigurationSupport {
    @Autowired
    private ObjectMapper objectMapper;

    // /**
    //  * 添加静态资源文件
    //  * <p>
    //  * 使用该方式会破坏SpringBoot默认加载静态文件的默认配置,需要重新进行添加. 切记
    //  * see: https://blog.csdn.net/liushangzaibeijing/article/details/82493910
    //  *
    //  * @param registry
    //  */
    // @Override
    // public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //     //重写这个方法,映射静态资源文件
    //     registry.addResourceHandler("/**")
    //             .addResourceLocations("classpath:/resources/")
    //             .addResourceLocations("classpath:/static/")
    //             .addResourceLocations("classpath:/public/");
    //     // super.addResourceHandlers(registry);
    // }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // super.configureMessageConverters(converters);
        // diy转换器, 序列化时对 bean properties null 值返回对应类型的初始值
        JacksonHttpMessageConverter jhmc = new JacksonHttpMessageConverter(objectMapper); // 用我自己定制的
        converters.add(jhmc);
        // 追加默认转换器
        // super.addDefaultHttpMessageConverters(converters);
    }

    // 注册自定义的 Converter
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 拿到配置所有 Converter 注册进去  GlobalDateTimeConfig
        Map<String, Converter> converterMap = SpringContextUtils.getApplicationContext().getBeansOfType(Converter.class);
        converterMap.forEach((k, v) -> {
            registry.addConverter(v);
            log.info("==> added converter: {}", k);
        });
    }

}

PS: 还是太太太麻烦,,,,,