转载

Jackson中基于上下文拦截属性输出的两种实现方式

需求如下,在一个类中,有一些字段属性,其是否输出并不是由字段上的JsonIgnore来决定,而是根据从上下文(如request)中传递过来的某些参数决定。如下类:

class A {
    @ContextIgnored("field1")
    private String field1;

    @ContextIgnored("field2")
    private String field2;
}

当上下文值为 field1 时,则表示 A 的最终输出json 中没有字段 field1. 当上下文为 field2 时, 则最终输出没有字段 field2. 其它情况则输出所有字段。

本文讨论Jackson中的处理方式,如果使用 Fastjson,则有很多方式处理,这里不表.

本文描述通过利用 JsonIgnore 注解处理方式解决此问题 和 通过 PropertyFilter 两种方式来完成此需求的处理方式。

利用 AnnotationIntrospector 中 hasIgnoreMarker 实现属性过滤

在标准的类处理中,jackson在处理每个类时,均会为此类生成1个 JsonSerializer 对象,在此场景中应该为 BeanSerializer 对象,而类中属性的处理,则是由 BeanPropertyWriter 来完成。即可以理解为在处理对象时,会先有1个解析类和属性的过程存在,如果在此过程中,检测到某个属性不应该最终输出,则不会产生相应属性的 BeanPropertyWriter 对象,即达到过滤输出的目的。

在正常的场景中,我们可以通过 注解 JsonIgnore, 将其加到属性上,即解析时即会过滤到属性。而实际实现,则是由类 JacksonAnnotationIntrospector 中 的 hasIgnoreMarker 来完成,则就是通过读取注解来判断属性是否应该被exclude掉。ObjectMapper中默认的 AnnotationIntrospector 即是 JacksonAnnotationIntrospector 来完成,但我们可以通过 方法 ObjectMapper.setAnnotationIntrospector 来重新指定自定义的实现。如下参考代码.

class VerifyIntrospetor extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        boolean f = super.hasIgnoreMarker(m);
        if(!f) {
            ContextIgnored anno =  _findAnnotation(m, ContextIgnoreed.class);
            if(anno.value() == "验证值")
                return true;
        }
        return f;
    }
}

如上代码,即在原来只处理注解 JsonIgnore 的基础之上,再加上处理 注解 ContextIgnored.

但是在Jackson中,此上的代码只能被调用1次,即针对单个类,ObjectMapper会缓存每个class所对应的 JsonSerializer 实例。当上下文改变时,ObjectMapper 并不会重新再创建新的 JsonSerializer,而是直接使用已经缓存过的 对象来处理,这样实际上就达不到最初的需求。相应的缓存类,可参考类 DefaultSerializerProvider.findTypedValueSerializer 方法实现 ,以及其中的类 SerializerCache. 由于当前jackson的实现, SerializerCache 为1个final类,并不支持扩展,因此修改 DefaultSerializerProvider 中属性的方式不可处理。

不过可以通过类似Composite的方式来解决此问题,通过方法 ObjectMapper.setSerializerProvider 可以重新使用1个新的 DefaultSerializerProvider 对象,此此类是可以被继承的。当 ObjectMapper 在writeValue时,均会调用 DefaultSerializerProvider 中 serializeValue 方法来进行序列化操作。更准确的说,会先调用 createInstance 方法产生1个新的 DefaultSerializerProvider 来完成对象序列化操作。那么在此过程中,可以通过1个类似 Map 来组合不同上下文的处理。即在 createInstance 时,根据当前上下文的值委派给具体的类来进行处理。而当委派之后,再由后面的 DefaultSerializerProvider.Impl 类来完成最终 JsonSerializer 的创建和处理过程。我们构建的 Composite 对象即完成了 上下文 中转换的中间隔离作用,即根据 上下文值 的不同会有多个 DefaultSerializerProvider 存在,但具体对象的内部缓存的 JsonSerializer 即会根据上下文的不同有自己的处理逻辑。

示意代码如下:

class Composite {
    private Map<上下文, DefaultSerializerProvider> map;
    public Composite createInstance(SerializationConfig config, SerializerFactory jsf) {
        return map.computeIfAbsent(上下文, k-> new DefaultSerializerProvider.Impl());
    }
}

在具体实现中, 还需要考虑 DefaultSerializerProvider.copy 方法,这个在具体使用时具体实现即可.

利用 PropertyFilter 拦截属性输出

相比第1种手法的复杂及晦涩(可以理解为直接怼),这种手法就比较简单了。自 jackson 2.3 之后,提供接口 PropertyFilter ,以及对应的注解 JsonFilter 来允许对特定的类在处理字段时,进行额外的过滤处理操作。所谓的过滤操作,即将属性的实际操作转交由接口来完成。其接口下的1个参考实现 SimpleBeanPropertyFilter 即提供了 包含 和 排除的实现代码。我们只需要实现方法 boolean include(BeanPropertyWriter writer) 即可完成判断是否输出某个属性的操作。

此 PropertyFilter 的使用即在类上,通过 JsonFilter 定义此类的过滤器, 然后通过 ObjectMapper.setFilterProvider(SimpleFilterProvider) 添加 具体实现即可, 两都通过 value 来完成匹配过程。

PropertyFilter 接口是在每一次属性序列化均会处理,即意味着,如果方法 include(BeanPropertyWriter writer) 的返回值是变化的,即完成了动态拦截属性的作用。参考实现如下:

public class ContextIgnoredFilter extends SimpleBeanPropertyFilter {
    @Override
    protected boolean include(BeanPropertyWriter writer) {
        ContextIgnored anno = writer.getAnnotation(ContextIgnored.class);
        //上下文值与注解值判断处理...

        return true;
    }
}

这种实现方法,不需要添加额外的 AnnotationIntrospector, 也不需要调整 ObjectMapper 的 DefaultSerializerProvider 属性,并且在代码Review上更简单易懂。

原文  https://www.iflym.com/index.php/code/201906190001.html
正文到此结束
Loading...