因项目需求,需要开发一个方法级别的注解,有点类似于@Cacheable注解(暂时就叫@ZSCacheable),缓存到redis时可以选择过滤实体类的字段,也就是在实体类某字段上加@ZSCacheable这个字段才能被序列化,未添加此注解的字段不序列化,所以就涉及 字段过滤 的问题。

在字段过滤中猪哥使用Ignore失效了,最后通过debug+源码查看,找出了原因,结论如下:

结论: JsonGetter、JsonProperty、JsonSerialize.class、JsonView.class、JsonFormat.class、JsonTypeInfo.class、JsonRawValue.class、JsonUnwrapped.class、JsonBackReference.class、JsonManagedReference.class 这些注解会使Ignore失效,这是jackson设计如此,分析如下!

一、字段过滤的方案

尝试过两种方法:

方案1:过滤器

在实体类上加上​ ​@JsonFilter("myFilter")​ ​注解,然后在注解切面中加上拦截器,核心代码如下:

ObjectMapper om = new ObjectMapper();
SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider().setFailOnUnknownId(true);
// 这里添加自定义拦截器
simpleFilterProvider.addFilter("myFilter", new SimpleBeanPropertyFilter() {
@Override
protected boolean include(PropertyWriter writer) {
return writer.getAnnotation(ZSCacheable.class) != null;
}
});

om.writer(simpleFilterProvider);
try {
String dataStr = om.writeValueAsString(data);
stringRedisTemplate.opsForValue().set(dataKey, dataStr, expire, TimeUnit.SECONDS);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

但是这样会有个问题:其他接口调序列化此实体类时,也会查找​ ​myFilter​ ​拦截器,这样就会影响其他接口,我不希望影响到其他接口,所以这个方案行不通!

方案2:内省器

内省器不需要在实体类上加注解,不影响其他的接口和操作,非常适合我的需求,所以最后猪哥选择自定义内省器的方案。

private final ObjectMapper objectMapper = new ObjectMapper();

{
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 自定义内省器
objectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
private static final long serialVersionUID = 746160236290648021L;

// 忽略不带@ZSCacheable注解的字段
@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
if (super.hasIgnoreMarker(m)) {
return true;
}
if (m instanceof AnnotatedField) {
ZSCacheable zsCacheable = m.getAnnotation(ZSCacheable.class);
return zsCacheable == null;
}
return false;
}
});
}

上面代码中重载了​ ​hasIgnoreMarker​ ​​方法,在方法中增加判断字段中是否包含​ ​ZSCacheable​ ​注解,如果不包含则忽略。

看起来很美好,但是发现在序列化时带上了父类中的​ ​createTime​ ​​ 和 ​ ​updateTime​ ​​字段,查看父类发现这两个字段加上了​ ​@JsonFormat​ ​对时间格式化的注解,猜想可能是这个注解影响了序列化,是jackson的bug还是有意为之呢?具体原因还需要debug查看源码一探究竟。

二、探究为什么Ignore失效

带着猜想,猪哥debug一点一点看源码,最后发现这是jackson有意为之,jackson在自省器内部定义了两个class数组:​ ​ANNOTATIONS_TO_INFER_SER​ ​​ 和 ​ ​ANNOTATIONS_TO_INFER_DESER​ ​,在扫描实体类字段的时候会判断字段或者方法上是否有这些注解,如果有就取消Ignore,那具体是在哪里将取消Ignore的?猪哥带大家看看流程。

Jackson的@JsonIgnore失效原因探究及解决方案_json

1. 扫描所有字段

jackson在序列化实体类时,会扫描所有字段、方法,我们进入到添加字段的方法里面看看。

Jackson的@JsonIgnore失效原因探究及解决方案_redis_02


添加字段方法中,定义了一个​ ​PropertyName​ ​​后面是否取消Ignore是根据这个属性来的,我们重点看看查找Name的方法:​ ​findNameForSerialization​ ​。

Jackson的@JsonIgnore失效原因探究及解决方案_java_03


在​ ​findNameForSerialization​ ​中的赋值顺序逻辑是:

  1. ​JsonGetter​ ​​注解,如果有则把​ ​JsonGetter​ ​​注解的value赋值给​ ​PropertyName​ ​ ​ ​JsonProperty​ ​​注解,如果有则把​ ​JsonProperty​ ​​注解的value赋值给​ ​PropertyName​
  2. 如果上面上个注解都没有,再判断注解上有没有默认的几个注解,如果有则给一个则把空字符串赋值给​ ​PropertyName​
  3. Jackson的@JsonIgnore失效原因探究及解决方案_json_04


  4. Jackson的@JsonIgnore失效原因探究及解决方案_字段_05

  5. 当字段上有上面那几个注解后,​ ​findNameForSerialization​ ​方法会将空字符串赋值给​ ​PropertyName​ ​,然后后面会判断​ ​PropertyName​ ​是否为空字符串,如果为空字符串(且不为null)则把字段名赋值给​ ​PropertyName​ ​。
  6. Jackson的@JsonIgnore失效原因探究及解决方案_字段_06


至此,我们已经清楚了jackson扫描字段的逻辑:如果字段上有上面那几个注解,​ ​PropertyName​ ​​则为字段名,如果没有那几个注解​ ​PropertyName​ ​​则为null,后面判断是否要取消Ignore就是根据​ ​PropertyName​ ​来判断的。

2. 取消Ignore

扫描所有字段后,执行 删除不想要属性的方法,这里会判断他的​ ​PropertyName​ ​​和​ ​Ignora​ ​属性,我们进去看看

Jackson的@JsonIgnore失效原因探究及解决方案_java_07


在​ ​_removeUnwantedProperties​ ​​方法中,有一个判断是否明显引入的方法​ ​isExplicitlyIncluded​ ​​,如果明显引入那就删除Ignore,使Ignore失效,我们进入​ ​isExplicitlyIncluded​ ​看看

Jackson的@JsonIgnore失效原因探究及解决方案_java_08


在​ ​isExplicitlyIncluded​ ​​方法的逻辑就是判断​ ​PropertyName​ ​不为空!

Jackson的@JsonIgnore失效原因探究及解决方案_java_09


Jackson的@JsonIgnore失效原因探究及解决方案_java_10


Jackson的@JsonIgnore失效原因探究及解决方案_jackson_11


看到这里我们就明白了jackson 取消Ignore的流程了:取消Ignore的根据就是​ ​PropertyName​ ​​是否有值, 而在扫描所有字段(方法)时带那几个注解的​ ​PropertyName​ ​就有值。

三、解决方案

我们知道在扫描字段时,如果字段带上了那几个注解,​ ​findNameForSerialization​ ​​会返回一个空字符串,如果不带那几个注解则返回null,所以我们可以重写​ ​findNameForSerialization​ ​方法:当返回空字符串时我们直接返回null。

Jackson的@JsonIgnore失效原因探究及解决方案_redis_12


重写​ ​findNameForSerialization​ ​​方法,先执行父类原来的逻辑,如果​ ​PropertyName​ ​​为null或者空,则返回null,这样后面的逻辑就不会给​ ​PropertyName​ ​赋值了,最后也就不会取消Ignore了。

Jackson的@JsonIgnore失效原因探究及解决方案_redis_13

四、对比效果

解决前:

Jackson的@JsonIgnore失效原因探究及解决方案_jackson_14


解决后:

Jackson的@JsonIgnore失效原因探究及解决方案_jackson_15