现在的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 {
}
}