SpringBoot中Jackson序列化处理自定义注解

文章目录

  • 背景
  • 实现步骤
    • 自定义注解
    • 自定义处理注解的序列化器
    • 自定义注解内省器
    • Jackson序列化配置
  • 测试
  • 思考
    • 如何找到处理自定义注解的序列化器
    • 如何向序列化器传递自定义注解中的参数
    • 如何不影响已有的Jackson序列化配置

背景

上篇文章 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 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;
    }

你可能感兴趣的:(Java,基础,Jackson,spring,boot,java,spring)