一个注解引发的血案

来源:互联网 发布:ubuntu 手机助手 编辑:程序博客网 时间:2024/06/05 11:07

一. 案发现场

这天晚上正在家里看书,突然公司带来一个电话,一个师兄和我说日常环境出现了空指针问题,要我有空明天看一下,于是我立刻上机器上看了下,是一个自定义缓存类抛出的异常:
这里写图片描述
最近在开发的项目里使用了SpringCache 的缓存组件,并且实现了自定义的缓存:

<bean id="tairCacheContainer" class="com.taobao.film.common.cache.TairCacheContainer"/><!-- generic cache manager --><bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">    <property name="caches">        <set>            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default" />            <ref bean="tairCacheContainer"/>        </set>    </property></bean>

而自定义缓存tairCacheContainer,所以抛出异常的正是这个自定义缓存类。看了错误堆栈之后就开始对问题进行分析了。

二. 排查过程

a. 读取错误堆栈,找到错误代码对应的行数,是对重写的put 方法中的一行

if(methodAnno != null) {    CacheSecond cs =(CacheSecond)methodAnno.getAnnotation(CacheSecond.class);    if(cs != null) {        cacheSecond = cs.value();    } else {        RemoteCache remoteCache = (RemoteCache)methodAnno.getAnnotation(RemoteCache.class);        cacheSecond = remoteCache.cacheSecond();    }}} catch (Throwable var20) {    logger.error("parse class error! ", var20);    cacheSecond = 300;}

异常是在这个catch里捕捉的,而报的内容是空指针。由此可见,可能是 remoteCache对象 为null导致的。同时可以看到这里有catch进行了兜底处理,及时有空指针或者其他异常,也能继续赋值缓存的时间,所以对接口的影响其实不大。

b. 为了证实刚才的猜想,已经排查出现这个问题的原因,我在本地的代码进行了Junit单测,debug了一个出现问题接口的流程,结果如下图:
这里写图片描述
缓存生成的key是函数名+函数参数,说明不是重载等问题导致反射去注解导致了取的是同名函数的第一个,引起的空指针(这个是那位师兄提醒我的可能原因之一)。
这里写图片描述
在这张图里可以看到remoteCache对象确实为空,验证了刚才的猜想。
这里写图片描述
接下来就进入了catch的代码块,把捕获的异常给打印了出来。

c. 看了下这段源码的意思,主要是从我们在业务函数上标注的注解@RemoteCache中通过反射取出一个缓冲的有效时间,再赋值,最后
tairManager.put(this.tairArea, keyString, (Serializable)value, 0, cacheSecond); 将参数赋值在tair里,用Spring Cache的扩展实现了自定义的tair缓存系统。看业务代码里都标注上了这个注解,所以不会是因为没有标注注解引起的。如果还会空指针,让我不由的怀疑起了是class本身的问题,于是我看了下两个class引用的地方:
这里写图片描述
这里写图片描述
这里可以发现对比了RemoteCache.class 在TairCacheContainer中是 com.taobao.film.common.cache.annotation包下的,而我们的项目下引用的包都是在 com.taobao.tfkapalai.biz.cache包下。因为它们两个本来就不是一个class,所以TairCacheContainer使用反射就没有拿到它想要的RemoteCache对象。

d.修改了项目里的注解引用的包之后,日志再也没有这个报错,问题得到了解决。

三. 案情总结

这次的问题是由于项目的老代码里使用了一个自定义缓存组件扩展了Spring Cache,业务代码里导入注解的包和组件里注解的包不是同一个包。这个问题上线了许久都没有被及时发现,原因是因为组件的源码里有catch这个异常,出异常时进行了重新赋予缓存有效时间的兜底。
这样编写代码有规范上的隐患:在项目开发过程中如果引用了外部的依赖,Import Class 发现了有多个选项时,直接选择一个自己本地的类,就会出现这种不该出现的问题。在对例如Spring Cache这种组件的使用时要注意看它的配置和涉及的类,了解到它引用的类到底是哪些后再决定要导入的类才是正确的做法。