警惕!自定义注解使用不当的排查实录
一、引言
大家好,在日常开发过程中,Java 注解(Annotation)是开发中经常使用的一个手段,用于给代码添加元数据的标记。它们可以提供代码额外的信息,这些信息可以在编译时或运行时被访问。注解不会改变代码的执行逻辑,但可以被编译器、JVM 或框架等工具用于生成额外的代码、提供警告或执行其他操作。注解虽然简单,但在平时开发过程中也会遇到各种各样的问题,本人有幸也遇到过,在此与大家分享一次遇到的注解相关问题,如有错误,还请各位大佬们指正。
二、排查过程
在一次分析接口性能时候,发现老业务代码中以下方法是有添加缓存注解,但是并没有起到缓存的作用,在缓存到期之前仍然去请求了下游 JSF 接口获取数据,业务代码如下:
![](https://static001.geekbang.org/infoq/a8/a817dbd2e2e0799c1fef77847311872d.png)
可以看到,该方法 getRiskInfoByPerformanceAccount,是根据入参做了内存及 Redis 缓存的功能。但是在查看接口 pfinder 调用链路时,发现同一个入参仍然重复了 9 次去调用下游 JSF 接口查询。
![](https://static001.geekbang.org/infoq/26/26d4c770f4aaadb764b208289f5f6055.png)
既然加了缓存注解,为什么会失效呢,下面开始分析一下具体缓存的代码逻辑:
下面代码是该缓存功能的拦截器实现:
![](https://static001.geekbang.org/infoq/13/138cbe98a5830b6971a1892823e18752.png)
首先,会把所有的缓存实现放入到实现链中,缓存实现即是去对应的位置(redis、内存、接口等)查询数据;
![](https://static001.geekbang.org/infoq/c5/c531f0509500780fdd57a16533dd1872.png)
然后,再把无缓存的查询实现链加入到列表最后。这个流程正常看也是没有问题的。
通过观察发下,这个接口会有返回 null 的情况,
![](https://static001.geekbang.org/infoq/0a/0a2cb56281ddd38577bae8c3825d5209.png)
那会不会可能 value 是 null 导致了缓存不上呢,继续分析缓存实现逻辑,可以看到缓存实现链执行:
![](https://static001.geekbang.org/infoq/7f/7fcec80f16f70e9b982d8eb64be70a61.png)
咱们开始一条一条链路看:
内存缓存实现,可以看到直接跳到下一个(redis)实现链:
![](https://static001.geekbang.org/infoq/34/34e3911ec02fe63fe1a0654bc9dcc279.png)
那咱们继续看 redis 实现链,存储到 redis 里面是一个对象包含过期时间、缓存值、key 等,
![](https://static001.geekbang.org/infoq/c6/c6a954bb087614e11ac039cc03e30481.png)
先打了个 hitRedisCache 标记 false,然后通过 getValueFromRedisAndConvert 方法去查(该方法也并没有对查出来空值的处理),查到且没过期则返回;没查到继续去进入下一个实现链(查询接口:获取具体缓存值的调用 RPC 或数据库等)。再看写入 redis 的逻辑:
![](https://static001.geekbang.org/infoq/9d/9da5c241ba57a05911fead955ff91f48.png)
调查询接口接口获取到的 value 也并没有空等判断,直接设置缓存对象里放入 redis 里。
到此,代码上分析整个流程没发现有什么问题,随后开展了本地调试,调用了两次带 cache 注解的方法,理论上第二次不会去调查询接口了,通过本地调试也的确没有调用查询接口。那就比较奇怪了,为什么本地测试没问题,测试、预发及线上环境都有这个问题。头大了,想了想还是去测试环境试试,然后在切面类中加上日志去测试环境进行测试,发现测试环境并没有打印日志,说明没进入到切面实现类里面,奇怪了,明明加了注解,为什么没进到切面呢,问题肯定还是注解上的问题,回去继续看代码吧:
![](https://static001.geekbang.org/infoq/fe/febc8b5d81a2a0b6c9a1319770d8eb16.png)
发现加 cache 注解的方法只有本类中的其他方法调用,并没有其他类调用,至此,问题就比较清晰了。
三、解决思路
上述问题是通过普通的方法调用方式调用目标方法,切面是不会生效的,因为切面主要应用于通过 Spring AOP 或其他代理机制进行的方法调用。在同一个类中的方法调用不会经过代理,因此切面也不会被触发。可以考虑将目标方法提取到一个单独的类中,并通过依赖注入的方式调用目标方法,以确保切面能够生效。
经过修改后,已经可以成功缓存结果,日志验证如下:
![](https://static001.geekbang.org/infoq/8a/8ad44800d0b09c28936fbefceeda681c.png)
四、总结分析
Java 注解固然可以为我们提供方便,但是需要注意使用场合,不是来个场景就使用注解。下面是一些具体的使用注意事项,供大家参考:
◦避免循环依赖:不要在注解中使用可能导致循环依赖的类或接口。
◦不要过度使用注解:注解可以提高代码的可读性和维护性,但过度使用会导致代码复杂。注解适合用于描述简单的元数据信息,对于复杂的业务逻辑,过度使用注解会导致代码难以理解和维护。如果需要在运行时动态修改逻辑,注解并不适合,因为它们在编译时就已经确定了。如果需要根据复杂的条件进行逻辑判断,这种情况下使用注解可能会使代码难以阅读和理解也可能造成频繁修改不利于代码维护。
◦反射使用:如果需要在运行时通过反射读取注解,确保注解的保留策略至少是 RetentionPolicy.RUNTIME。
◦重写注解:当重写父类方法时,如果父类方法上有注解,需要考虑是否需要在子类方法上也添加相同的注解。
◦注解继承:注解不会被子类自动继承,如果需要在子类中使用,必须显式添加。
◦避免重复注解:Java 8 引入了重复注解的概念,但在之前版本中,不能在同一个元素上多次使用同一个注解。
◦类型检查:在使用注解时,确保类型检查正确,例如,不要将 int 类型的属性值赋予 string 类型的注解属性。
◦性能考虑:运行时注解处理可能会影响性能,尤其是在大量使用反射的情况下。
遵循上面这些使用注释事项,可以帮助大家更有效地使用 Java 注解,同时保持代码的清晰和可维护性。
作者:京东物流 刘邓忠
来源:京东云开发者社区
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/d2312a3d39581d44186678527】。文章转载请联系作者。
评论