当前位置: 首页 > 工具软件 > Jackson > 使用案例 >

【Jackson】自定义注解结合Jackson

齐泰
2023-12-01

自定义注解结合Jackson

虽然最终的解决方法挺简单的,不过解决问题过程还是挺有趣的,在此记录一下。

有时候我们希望我们自定义的注解同时拥有 Jackon 注解的功能。

例如我们有这么一个功能,需要自定义注解来标注类属性,来达到批量解析类属性的目的,而且并且我们希望被这个注解标志的属性不被 Json 序列化(不希望返回给前端)。通常情况下我们是不是会这么写

public class MyClass {
	
	@CustomAnnotation // 我们自定义的注解
	@JsonIgnore
	private String myField;
	
}

如果这种需要被@CustomAnnotation标注的地方有很多,这么来写就显得很啰嗦麻烦了,那么有没有办法能够让@CustomAnnotation同时拥有@JsonIgnore的特性呢?这样只需要写一个注解就可以了。Jackson作为Springboot中使用广泛使用并且作为SoringBoot默认的Json解析框架来说肯定也是支持的。

1. 解决方法

在自定义注解上添加 @JacksonAnnotationInside 注解,这样Jackson会把被@JacksonAnnotation标注的注解作为一个组合注解,并扫描该注解上的注解,这样我们在该注解上的Jackson相关的注解就会起作用。
有兴趣的可以看看下面解决的思路。

例如

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JsonIgnore
@JacksonAnnotationsInside  // 标注为组合注解
public @interface CustomAnnotation {
}

2. 背景

起因是公司的一个项目,功能如下:

  1. 需要指定各种指标类,每一个指标都需要进行指标计算
  2. 这些指标类每一个都有各种类型不同、数量不同的配置
  3. 指标配置内容存储到数据库中
  4. 后端从数据库读取配置加载配置给指标后做指标计算。
  5. 最后将指标信息和计算结果返回给前端

首先指标不同的配置,数据类型不一定、数量不一定,如何做到指标易扩展,而且这些配置自动装配呢?

我的做法做一个注解,该注解控制配置解析逻辑,并且这些配置数据中间结果,不想暴露给前端,所以它也要具备 @JsonIgnore的功能。

3. 程序逻辑

定好逻辑后,我定义了自定义的注解,该注解标识了对应指标的配置在数据库配置的第几个位置,并在基础指标类中进行解析与设置指标配置。这样扩展子指标的时候只需要继承基础指标类并用注解标注怎么取配置,从而就可以专心写指标计算逻辑。

于是我定义的注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JsonIgnore
public @interface IndexParam {

    /**
     * 这个属性标识了被标识的Field所对应的配置在数据库参数配置中占第几个位置
     */
    int value() default -1;
}

解析逻辑如下(基础指标BaseIndex中):

private void resetParam(ObjectArray config) {
    // config 是从数据库解析到的配置列表,不是重点
    // 扫描具有IndexParam注解的Field,设置值
    BeanUtil.filterFieldAnnotation(this, IndexParam.class, (field, annotation) -> {
    	// 获取该属性的配置在系统配置的哪一个位置
        int fieldIndex = annotation.value();
        if (fieldIndex == -1) {
            return;
        }
        try {
            // 获取配置,并设置值
            field.set(this, config.castGet(fieldIndex));
        } catch (IllegalAccessException e) {
            log.error("初始化{}({})指标参数错误{}", index.getName(), field.getName(), e);
        }
    });
}

使用方式如下(具体指标实现类):

public class YlValueWarnNumIndex extends BaseIndex {

    @IndexParam(value = 0)
    private double shallowYellowLimit;
    @IndexParam(value = 1)
    private double shallowRedLimit;

    @IndexParam(value = 2)
    private double deepYellowLimit;
    @IndexParam(value = 3)
    private double deepRedLimit;


    @IndexParam(value = 4)
    private double yellowWarnNum;
    @IndexParam(value = 5)
    private double redWarnNum;

	public YlValueWarnNumIndex(WarnIndex index, AreaIndexConfig config) {
	   // 父类中负责初始化上面的所有属性,所以下面的calculateIndexResult方法里面无需解析就可以直接使用上面的6个配置项
       super(index, config);
   }

  	@Override
    protected void calculateIndexResult(AlgoHistoryInfo info) {
		// do index calculate
	}
}

4. 问题

就在我写完逻辑测试的时候,我发现这些被 @IndexParam 标注的 属性最后还是返回给了前端也就是在@IndexParam中设置的@JsonIgnore没有起作用。

在网上搜怎么在自定义注解中使用Jackson的注解,结果搜出来全都是怎么自定义注解,凭直觉觉得Jackson肯定是支持这种功能的,所以没办法只能自己debug追踪Jackon的源码。于是一步一步追踪发现了 Jackson通过判断注解有没有 @JacksonAnnotationsInside 注解进而判断是否需要进一步解析注解的注解。所以只要给自定义的注解加上 @JacksonAnnotationsInside 注解就可以让我们自定义注解上的@JsonIgnore起作用了。

下面是Jackson对应部分的源码:

5. Jackson注解扫描与组合注解源码

CollectorBase : 用来收集类中属性信息的Collector
@JacksonAnnotationsInside : 用于自定义注解组合注解

1. 注解扫描

Jackson 在序列化对象的时候,会扫描类、类属性、Getter、Setter上标记的注解,例如@JsonIgnore、@JsonAlias、@JsonFormat …,并根据对应的注解进行对应的序列化处理。

Jackson的注解扫描通过 CollectorBase 实现,不同类型的注解扫描通过继承 CollectorBase类实现。

AnnotatedCreatorCollector 负责扫描类的静态方法和构建方法。

AnnotatedFieldCollector 负责扫描类的内部属性

AnnotatedMethodCollector 负责扫描类的内部方法

2. 组合注解

CollectorBase 在扫描注解的时候,如果发现注解是组合注解(被@JacksonAnnotationsInside 标识),那么会收集该注解的所有注解信息,如果不是组合注解则直接加入到收集器中,不进行扫描它的注解信息

3. 源码

/**
 *   Class: CollectorBase
 *   Jackson 的注解扫描收集器,收集需要被序列化的类相关的各种注解
 */
protected final AnnotationCollector collectAnnotations(AnnotationCollector c, Annotation[] anns) {
    for (int i = 0, end = anns.length; i < end; ++i) {
        // 将当前注解添加到收集器中
        Annotation ann = anns[i];
        c = c.addOrOverride(ann);
        // 判断是否是组合注解(判断当前注解是否被 @JacksonAnnotationsInside 注解标识)
        if (_intr.isAnnotationBundle(ann)) {
            // 收集所有的组合注解
            c = collectFromBundle(c, ann);
        }
    }
    return c;
}

/**
 * Class: JacksonAnnotationIntrospector
 * 判断是否是组合注解(判断依据:是否被 @JacksonAnnotationsInside 注解标识)
 */
@Override
public boolean isAnnotationBundle(Annotation ann) {
    Class<?> type = ann.annotationType();
    // 使用Lru缓存加速判断速度(_annotationsInside,处于Jackson util包下的Lru缓存实现类)
    Boolean b = _annotationsInside.get(type);
    if (b == null) {
        // 判断是否被 @JacksonAnnotationsInside 标识
        b = type.getAnnotation(JacksonAnnotationsInside.class) != null;
        _annotationsInside.putIfAbsent(type, b);
    }
    return b.booleanValue();
}

/**
 * Class: CollectorBase
 * 递归扫描组合注解
 */
protected final AnnotationCollector collectFromBundle(AnnotationCollector c, Annotation bundle) {
    // 获取当前注解的所有注解列表
    Annotation[] anns = ClassUtil.findClassAnnotations(bundle.annotationType());
    // 遍历注解列表
    for (int i = 0, end = anns.length; i < end; ++i) {
        Annotation ann = anns[i];
        // 如果是Jdk自带的元注解,忽略(因为这几个会存在循环注解扫描调用的问题)
        if (_ignorableAnnotation(ann)) {
            continue;
        }
        // 判断是否是组合注解
        if (_intr.isAnnotationBundle(ann)) {
            // 如果该注解已经被扫描过了,跳过。也是为了防止出现注解循环扫描调用问题
            if (!c.isPresent(ann)) {
                // 添加到收集器中
                c = c.addOrOverride(ann);
                // 继续递归扫描
                c = collectFromBundle(c, ann);
            }
        } else {
            // 如果不是组合注解直接添加到收集器中
            c = c.addOrOverride(ann);
        }
        
    }
    return c;
}
 类似资料: