警惕!自定义注解使用不当的排查实录
一、引言
大家好,在日常开发过程中,Java 注解(Annotation)是开发中经常使用的一个手段,用于给代码添加元数据的标记。它们可以提供代码额外的信息,这些信息可以在编译时或运行时被访问。注解不会改变代码的执行逻辑,但可以被编译器、JVM 或框架等工具用于生成额外的代码、提供警告或执行其他操作。注解虽然简单,但在平时开发过程中也会遇到各种各样的问题,本人有幸也遇到过,在此与大家分享一次遇到的注解相关问题,如有错误,还请各位大佬们指正。
二、排查过程
在一次分析接口性能时候,发现老业务代码中以下方法是有添加缓存注解,但是并没有起到缓存的作用,在缓存到期之前仍然去请求了下游JSF接口获取数据,业务代码如下:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16pPIS7TRuw11GqMGT.png)
可以看到,该方法getRiskInfoByPerformanceAccount,是根据入参做了内存及Redis缓存的功能。但是在查看接口pfinder调用链路时,发现同一个入参仍然重复了9次去调用下游JSF接口查询。
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16lsoRTyT3F8lzpAf.png)
既然加了缓存注解,为什么会失效呢,下面开始分析一下具体缓存的代码逻辑:
下面代码是该缓存功能的拦截器实现:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16op03EpRdwAZ316Te.png)
首先,会把所有的缓存实现放入到实现链中,缓存实现即是去对应的位置(redis、内存、接口等)查询数据;
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16qlR9dK6xmzidEej.png)
然后,再把无缓存的查询实现链加入到列表最后。这个流程正常看也是没有问题的。
通过观察发下,这个接口会有返回null的情况,
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16kEXF7DPd16gsX0uk.png)
那会不会可能value是null导致了缓存不上呢,继续分析缓存实现逻辑,可以看到缓存实现链执行:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16fc3ocveJSwajc9y.png)
咱们开始一条一条链路看:
内存缓存实现,可以看到直接跳到下一个(redis)实现链:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-1686nYnhmgGI117OyX.png)
那咱们继续看redis实现链,存储到redis里面是一个对象包含过期时间、缓存值、key等,
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-162z16VSChNeNYppbK.png)
先打了个hitRedisCache标记false,然后通过getValueFromRedisAndConvert方法去查(该方法也并没有对查出来空值的处理),查到且没过期则返回;没查到继续去进入下一个实现链(查询接口:获取具体缓存值的调用RPC或数据库等)。再看写入redis的逻辑:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-163fkkiULWGNbGrRc.png)
调查询接口接口获取到的value也并没有空等判断,直接设置缓存对象里放入redis里。
到此,代码上分析整个流程没发现有什么问题,随后开展了本地调试,调用了两次带cache注解的方法,理论上第二次不会去调查询接口了,通过本地调试也的确没有调用查询接口。那就比较奇怪了,为什么本地测试没问题,测试、预发及线上环境都有这个问题。头大了,想了想还是去测试环境试试,然后在切面类中加上日志去测试环境进行测试,发现测试环境并没有打印日志,说明没进入到切面实现类里面,奇怪了,明明加了注解,为什么没进到切面呢,问题肯定还是注解上的问题,回去继续看代码吧:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16OvAxJPJGK11ovMFV.png)
发现加cache注解的方法只有本类中的其他方法调用,并没有其他类调用,至此,问题就比较清晰了。
三、解决思路
上述问题是通过普通的方法调用方式调用目标方法,切面是不会生效的,因为切面主要应用于通过 Spring AOP 或其他代理机制进行的方法调用。在同一个类中的方法调用不会经过代理,因此切面也不会被触发。可以考虑将目标方法提取到一个单独的类中,并通过依赖注入的方式调用目标方法,以确保切面能够生效。
经过修改后,已经可以成功缓存结果,日志验证如下:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-03-02-11-16Fw8xShWC0rpVj6f.png)
四、总结分析
Java 注解固然可以为我们提供方便,但是需要注意使用场合,不是来个场景就使用注解。下面是一些具体的使用注意事项,供大家参考:
遵循上面这些使用注释事项,可以帮助大家更有效地使用Java注解,同时保持代码的清晰和可维护性。
下面举例一些日常容易出现使用不当的注解:
@Transactional :当事务注解代码范围的逻辑中有大事务、长事务情况下,可能导致数据库死锁,系统性能极速下降等风险;
@Async :如过度使用可能导致线程过多内存CPU的使用率增长较大;如代码中有阻塞操作可能导致线程无法释放等风险。
@自定义注解:当使用自定义注解(例如权限验证、参数转换等),特别是使用三方的自定义注解,一定要先了解其使用范围、使用限制等,避免因不了解而造成线上事故。
上述举例并非全部的案例,也并未详细展开,后续会再进行一些具体案例的分享,谢谢大家!
以上仅仅代表个人观点,一点愚见,还请大家批评指正!同时,欢迎大佬们一起来补充!