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