因项目需求,需要开发一个方法级别的注解,有点类似于@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的?猪哥带大家看看流程。
1. 扫描所有字段
jackson在序列化实体类时,会扫描所有字段、方法,我们进入到添加字段的方法里面看看。
添加字段方法中,定义了一个
PropertyName
后面是否取消Ignore是根据这个属性来的,我们重点看看查找Name的方法:
findNameForSerialization
。
在
findNameForSerialization
中的赋值顺序逻辑是:
-
JsonGetter
注解,如果有则把
JsonGetter
注解的value赋值给
PropertyName
JsonProperty
注解,如果有则把
JsonProperty
注解的value赋值给
PropertyName
-
-
如果上面上个注解都没有,再判断注解上有没有默认的几个注解,如果有则给一个则把空字符串赋值给
PropertyName
-
-
当字段上有上面那几个注解后,
findNameForSerialization
方法会将空字符串赋值给
PropertyName
,然后后面会判断
PropertyName
是否为空字符串,如果为空字符串(且不为null)则把字段名赋值给
PropertyName
。
-
至此,我们已经清楚了jackson扫描字段的逻辑:如果字段上有上面那几个注解,
PropertyName
则为字段名,如果没有那几个注解
PropertyName
则为null,后面判断是否要取消Ignore就是根据
PropertyName
来判断的。
2. 取消Ignore
扫描所有字段后,执行 删除不想要属性的方法,这里会判断他的
PropertyName
和
Ignora
属性,我们进去看看
在
_removeUnwantedProperties
方法中,有一个判断是否明显引入的方法
isExplicitlyIncluded
,如果明显引入那就删除Ignore,使Ignore失效,我们进入
isExplicitlyIncluded
看看
在
isExplicitlyIncluded
方法的逻辑就是判断
PropertyName
不为空!
看到这里我们就明白了jackson 取消Ignore的流程了:取消Ignore的根据就是
PropertyName
是否有值, 而在扫描所有字段(方法)时带那几个注解的
PropertyName
就有值。
三、解决方案
我们知道在扫描字段时,如果字段带上了那几个注解,
findNameForSerialization
会返回一个空字符串,如果不带那几个注解则返回null,所以我们可以重写
findNameForSerialization
方法:当返回空字符串时我们直接返回null。
重写
findNameForSerialization
方法,先执行父类原来的逻辑,如果
PropertyName
为null或者空,则返回null,这样后面的逻辑就不会给
PropertyName
赋值了,最后也就不会取消Ignore了。
四、对比效果
解决前:
解决后: