[Spring Boot]Spring Boot Json序列化终极配置

1,658 阅读5分钟

现在的Web应用都是通过json序列化,SpringBoot内置Jackson工具提供json -> bean 或者 bean -> json序列化的。在SpringBoot序列化中,我们可以自定义json的序列化过程,比如增加脱敏,忽略字段,格式化时间,类型转化等。

解决数据库bigint类型JS会丢失精度问题

如果我们的数据库主键采用雪花算法生成的id,该id在数据库中是bigint类型,在Java代码中的表现形式是Long类型。但是我们把这个id传给前端的话,前端用JS接受会丢失精度,使得最后两位变成0,这就造成了前端在用这个id进行操作的时候,会给我们一个错误的id,如果要解决这个方法,有两种方式。

  • 方式一
// 注解处理,这里可以配置公共 baseEntity 处理
@JsonSerialize(using=ToStringSerializer.class)
public long getId() {
    return id;
}
  • 方式二
//添加对长整型的转换关系
ObjectMapper objectMapper = new ObjectMapper();
ToStringSerializer stringSerializer = ToStringSerializer.instance;
simpleModule.addSerializer(BigInteger.class, stringSerializer);
simpleModule.addSerializer(Long.class, stringSerializer);
simpleModule.addSerializer(Long.TYPE, stringSerializer);
objectMapper.registerModule(simpleModule);

个人更推荐方式二,毕竟我本人并不喜欢在实体类里面加一些关于json的注解。

格式化时间

中国时间的格式一般返回yyyy-MM-dd HH:mm:ss  格式,实现时间格式化转换,也有好几种方式。

  • 方式一
private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";

ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
objectMapper.registerModule(javaTimeModule).registerModule(new ParameterNamesModule());
  • 方式二
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  • 方式三
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

个人更推荐方式一,对实体类或者VO、DTO类的代码没有侵入

忽略字段

在返回给前端的字段中,有可能我们会将数据库的实体类返回,实体类中,有一些无关紧要的字段,比如 createTime, updateTime, deleted等通用字段,我们可以在json序列化的时候忽略掉

// 设置过滤字段
SimpleBeanPropertyFilter fieldFilter = SimpleBeanPropertyFilter.serializeAllExcept("create_time", "update_time", "deleted");
SimpleFilterProvider filterProvider = new SimpleFilterProvider().addFilter("defaultValue", fieldFilter);
objectMapper.setFilterProvider(filterProvider).addMixIn(BaseEntity.class, PropertyFilterMixIn.class);

@JsonFilter("defaultValue")
interface PropertyFilterMixIn {
}

除了上述方法,也可以在实体类上添加@JsonIgnore注解,但是我不喜欢。

脱敏

利用json序列化,我们可以定制一些高级的功能,比如敏感字段不全部显示,中间显示 ***等,好比手机号: 156****2570这种效果。

  • 先定义一些枚举,表示要脱敏字段的类型
public enum SensitiveTypeEnum {


    /**
     * 身份证号
     */
    ID_CARD,
    /**
     * 密码
     */
    PASSWORD,
    /**
     * 手机号
     */
    MOBILE_PHONE,
    /**
     * 电子邮件
     */
    EMAIL,
    /**
     * 真实姓名
     */
    NAME,
    /**
     * 账户信息
     */
    ACCOUNT_NO;
}

  • 脱敏的工具类
public class SensitiveUtils {

    /**
     * [真实姓名] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
     */
    public static String realName(final String realName) {
        if (StringUtils.isBlank(realName)) {
            return "";
        }
        return dealString(realName, 1, 0);
    }

    /**
     * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
     */
    public static String idCard(final String idCard) {
        if (StringUtils.isBlank(idCard)) {
            return "";
        }
        return dealString(idCard, 3, 4);
    }

    /**
     * [手机号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
     */
    public static String mobilePhone(final String idCard) {
        if (StringUtils.isBlank(idCard)) {
            return "";
        }
        return dealString(idCard, 3, 4);
    }

    /**
     * [邮箱] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
     */
    public static String email(final String email) {
        if (StringUtils.isBlank(email)) {
            return "";
        }
        int index = email.indexOf("@");
        return dealString(email, 3, email.length() - index);
    }

    /**
     * [账号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>
     */
    public static String acctNo(final String idCard) {
        if (StringUtils.isBlank(idCard)) {
            return "";
        }
        final String name = StringUtils.left(idCard, 1);
        return StringUtils.rightPad(name, StringUtils.length(idCard), "*");
    }

    /**
     * [密码] 隐藏。<例子:*************>
     */
    public static String password(final String password) {
        if (StringUtils.isBlank(password)) {
            return "";
        }
        return "*";
    }


    private static String dealString(String str, int headOff, int tailOff) {
        int length = str.length();
        StringBuilder sb = new StringBuilder();
        final String head = StringUtils.left(str, headOff);
        String tail = StringUtils.right(str, tailOff);
        sb.append(head);
        int size = length - (headOff + tailOff);
        if (size > 0) {
            while (size > 0) {
                sb.append("*");
                size--;
            }
        }
        sb.append(tail);
        return sb.toString();
    }


    /**
     * 提供给外部进行直接脱敏处理
     * @param type
     * @param value
     * @return
     */
    public static String sensitveValue(SensitiveTypeEnum type, String value) {
        switch (type) {
            case NAME: {
                return realName(String.valueOf(value));
            }
            case ID_CARD: {
                return idCard(String.valueOf(value));
            }
            case MOBILE_PHONE: {
                return mobilePhone(String.valueOf(value));
            }
            case EMAIL: {
                return email(String.valueOf(value));
            }
            case ACCOUNT_NO: {
                return acctNo(String.valueOf(value));
            }
            case PASSWORD: {
                return password(String.valueOf(value));
            }
            default:
                return String.valueOf(value);
        }

    }


}

此工具类可以单独使用

  • 将脱敏过程绑定到json序列化
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.storyhasyou.kratos.annotation.Sensitive;
import com.storyhasyou.kratos.enums.SensitiveTypeEnum;
import com.storyhasyou.kratos.utils.SensitiveUtils;

import java.io.IOException;
import java.util.Objects;


public class SensitiveSerialize extends JsonSerializer<Object> implements ContextualSerializer {

    private final SensitiveTypeEnum type;

    public SensitiveSerialize(final SensitiveTypeEnum type) {
        this.type = type;
    }

    public SensitiveSerialize() {
        this.type = SensitiveTypeEnum.NAME;
    }


    @Override
    public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {
        switch (this.type) {
            case ID_CARD: {
                jsonGenerator.writeString(SensitiveUtils.idCard(String.valueOf(value)));
                break;
            }
            case MOBILE_PHONE: {
                jsonGenerator.writeString(SensitiveUtils.mobilePhone(String.valueOf(value)));
                break;
            }
            case EMAIL: {
                jsonGenerator.writeString(SensitiveUtils.email(String.valueOf(value)));
                break;
            }
            case ACCOUNT_NO: {
                jsonGenerator.writeString(SensitiveUtils.acctNo(String.valueOf(value)));
                break;
            }
            case PASSWORD: {
                jsonGenerator.writeString(SensitiveUtils.password(String.valueOf(value)));
                break;
            }
            case NAME: {
                jsonGenerator.writeString(SensitiveUtils.realName(String.valueOf(value)));
                break;
            }
            default:
                jsonGenerator.writeString(String.valueOf(value));

        }

    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {

        if (beanProperty != null) {

            // 非 String 类直接跳过
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                Sensitive sensitiveInfo = beanProperty.getAnnotation(Sensitive.class);
                if (sensitiveInfo == null) {
                    sensitiveInfo = beanProperty.getContextAnnotation(Sensitive.class);
                }
                // 如果能得到注解,就将注解的 value 传入 SensitiveInfoSerialize
                if (sensitiveInfo != null) {

                    return new SensitiveSerialize(sensitiveInfo.value());
                }
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(beanProperty);

    }
}

  • 最后添加一个注解
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.storyhasyou.kratos.toolkit.SensitiveSerialize;
import com.storyhasyou.kratos.enums.SensitiveTypeEnum;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;


@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface Sensitive {

    SensitiveTypeEnum value() default SensitiveTypeEnum.PASSWORD;

}

将需要脱敏的字段,打上这个注解,就可以在json序列化中,进行脱敏操作。

一个生产环境json配置文件

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonInclude;
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.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.storyhasyou.kratos.base.BaseEntity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;


@Configuration
public class JacksonConfig {

    private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        ObjectMapper objectMapper = new ObjectMapper();
        //定义Json转换器
        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
        // 设置Jackson序列化和反序列化的时候,使用下划线分割
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        objectMapper.registerModule(javaTimeModule).registerModule(new ParameterNamesModule());

        // 设置过滤字段
        SimpleBeanPropertyFilter fieldFilter = SimpleBeanPropertyFilter.serializeAllExcept("create_time", "update_time", "deleted");
        SimpleFilterProvider filterProvider = new SimpleFilterProvider().addFilter("defaultValue", fieldFilter);
        objectMapper.setFilterProvider(filterProvider).addMixIn(BaseEntity.class, PropertyFilterMixIn.class);

        SimpleModule simpleModule = new SimpleModule();
        //添加对长整型的转换关系
        ToStringSerializer stringSerializer = ToStringSerializer.instance;
        simpleModule.addSerializer(BigInteger.class, stringSerializer);
        simpleModule.addSerializer(Long.class, stringSerializer);
        simpleModule.addSerializer(Long.TYPE, stringSerializer);
        //将对象模型添加至对象映射器
        objectMapper.registerModule(simpleModule);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);
        //将对象映射器添加至Json转换器
        jackson2HttpMessageConverter.setObjectMapper(objectMapper);
        return jackson2HttpMessageConverter;
    }

    @JsonFilter("defaultValue")
    interface PropertyFilterMixIn {
    }


}