文章目录
背景
上篇文章 Jackson序列化带有注解的字段的原理浅析 里已经简单介绍了Jackson如何序列化带有注解的字段。
本文尝试自己定义一个注解,让Jackson序列化时对标注了该注解的字段进行脱敏。
实现步骤
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveField {
/**
* 敏感字段类型
* @return
*/
SensitiveFieldTypeEnum type();
}
public enum SensitiveFieldTypeEnum {
/**
* 身份证号
*/
ID_CARD,
/**
* 姓名
*/
NAME,
/**
* 手机号
*/
PHONE,
}
// 定义一个对象,字段标有注解
@Data
public class User {
@SensitiveField(type = SensitiveFieldTypeEnum.NAME)
private String name;
@SensitiveField(type = SensitiveFieldTypeEnum.PHONE)
private String phone;
private Integer age;
}
自定义处理注解的序列化器
@Log4j2
public class SensitiveFieldSerializer extends JsonSerializer<String> implements ContextualSerializer {
/**
* 脱敏字段类型
*/
private final ThreadLocal<SensitiveFieldTypeEnum> type = new ThreadLocal<>();
public SensitiveFieldSerializer() {
}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 不同的字段注解的参数type不同,type不同走不同的处理逻辑
SensitiveFieldTypeEnum sensitiveFieldTypeEnum = type.get();
if (sensitiveFieldTypeEnum == null) {
return;
}
switch (sensitiveFieldTypeEnum) {
case ID_CARD:
s = SensitiveDataUtils.encryptCard(s);
break;
case PHONE:
s = SensitiveDataUtils.encryptPhone((s));
break;
case NAME:
s = SensitiveDataUtils.encryptName(s);
break;
default:
break;
}
jsonGenerator.writeString(s);
// 用完主动remove
type.remove();
}
// 序列化器二次处理时调用该方法
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
// 获取注解的参数
SensitiveField annotation = beanProperty.getAnnotation(SensitiveField.class);
SensitiveFieldTypeEnum sensitiveFieldType = annotation.type();
// 将注解参数设置到序列化器的全局变量type里
type.set(sensitiveFieldType);
// 复用该序列器起对象
return this;
}
return serializerProvider.findNullValueSerializer(null);
}
}
- SensitiveDataUtils省略,就是一个实现字符串脱敏的工具类,可自行实现。
- 这里使用了
ThreadLocal<SensitiveFieldTypeEnum> type = new ThreadLocal<>();
,是因为多线程下该序列化器是复用的,但是传递的type会不一样,所以为了在判断type的时候不混乱,使用了ThreadLocal。
自定义注解内省器
@Log4j2
public class SensitiveFieldAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
SensitiveField annotation = am.getAnnotation(SensitiveField.class);
if (annotation != null) {
log.info("----当前序列化使用自定义敏感字段序列化器----");
return SensitiveFieldSerializer.class;
}
return super.findSerializer(am);
}
}
Jackson序列化配置
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector) {
// 根据已有的配置创建自定义的ObjectMapper
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
return objectMapper;
}
@Bean
public SensitiveFieldAnnotationIntrospector sensitiveFieldAnnotationIntrospector() {
return new SensitiveFieldAnnotationIntrospector();
}
@Bean
public SensitiveFieldSerializer sensitiveFieldSerializer() {
return new SensitiveFieldSerializer();
}
}
测试
@GetMapping("/get")
public User get() {
User user = new User();
user.setAge(20);
user.setName("徐小莫");
user.setPhone("18211843612");
return user;
}
执行结果:
{
"name": "***",
"phone": "182****3672",
"age": 20
}
思考
如何找到处理自定义注解的序列化器
Jackson序列化带有注解的字段的原理浅析 这篇文章已经提到要处理字段上的注解,就要通过注解內省器找到合适的序列化器。
所以我们自定义了一个注解內省器,并把它加入到了当前Jackson注解內省器集合里:
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
如何向序列化器传递自定义注解中的参数
Jackson序列化带有注解的字段的原理浅析 这篇文章提到序列化时的一个关键操作:序列化器二次处理。
正是通过二次处理,就能够将注解的参数(也可以理解为上下文)传递到序列化器中
如何不影响已有的Jackson序列化配置
实际的SpringBoot项目中,对于Controller的返回数据,经常会在配置文件或者配置类进行一些Jackson的自定义配置。
如果针对当前的自定义注解序列化器新建一个ObjectMapper
:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
SensitiveFieldSerializer sensitiveFieldSerializer = new SensitiveFieldSerializer();
simpleModule.addSerializer(String.class, sensitiveFieldSerializer);
objectMapper.registerModule(simpleModule);
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newIntro = AnnotationIntrospectorPair.pair(annotationIntrospector,new SensitiveFieldAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newIntro);
那这个ObjectMapper
不会包含原来的那些配置,因为他是一个新的对象。
要保证原来的配置生效,就需要对SpringBoot自动配置的ObjectMapper
动手脚。
通过分析JacksonAutoConfiguration.class
这个自动配置类:
@Bean
JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
this.customize(builder, customizers);
return builder;
}
@Bean
@Primary
// 这个注解代表没有自定义的ObjectMqpper bean对象时才会在这里创建
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
发现SpringBoot会将配置信息封装成Jackson2ObjectMapperBuilder
,进而去创建ObjectMapper
对象。
所以需要通过注入已有的Jackson2ObjectMapperBuilder
对象来创建我们自己的ObjectMapper
,再将自定义注解内省器加入到它的內省器集合,使我们的自定义注解序列化器生效:
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
return objectMapper;
}