开发者社区 > 博文 > 警惕!自定义注解使用不当的排查实录
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

警惕!自定义注解使用不当的排查实录

  • jd****
  • 2024-05-27
  • IP归属:北京
  • 100浏览

    一、引言

          大家好,在日常开发过程中,Java 注解(Annotation)是开发中经常使用的一个手段,用于给代码添加元数据的标记。它们可以提供代码额外的信息,这些信息可以在编译时或运行时被访问。注解不会改变代码的执行逻辑,但可以被编译器、JVM 或框架等工具用于生成额外的代码、提供警告或执行其他操作。注解虽然简单,但在平时开发过程中也会遇到各种各样的问题,本人有幸也遇到过,在此与大家分享一次遇到的注解相关问题,如有错误,还请各位大佬们指正。


    二、排查过程

    在一次分析接口性能时候,发现老业务代码中以下方法是有添加缓存注解,但是并没有起到缓存的作用,在缓存到期之前仍然去请求了下游JSF接口获取数据,业务代码如下:

    可以看到,该方法getRiskInfoByPerformanceAccount,是根据入参做了内存及Redis缓存的功能。但是在查看接口pfinder调用链路时,发现同一个入参仍然重复了9次去调用下游JSF接口查询。

    既然加了缓存注解,为什么会失效呢,下面开始分析一下具体缓存的代码逻辑:

    下面代码是该缓存功能的拦截器实现:

    首先,会把所有的缓存实现放入到实现链中,缓存实现即是去对应的位置(redis、内存、接口等)查询数据;

    然后,再把无缓存的查询实现链加入到列表最后。这个流程正常看也是没有问题的。


    通过观察发下,这个接口会有返回null的情况,

    那会不会可能value是null导致了缓存不上呢,继续分析缓存实现逻辑,可以看到缓存实现链执行:

    咱们开始一条一条链路看:

    内存缓存实现,可以看到直接跳到下一个(redis)实现链:

    那咱们继续看redis实现链,存储到redis里面是一个对象包含过期时间、缓存值、key等,

    先打了个hitRedisCache标记false,然后通过getValueFromRedisAndConvert方法去查(该方法也并没有对查出来空值的处理),查到且没过期则返回;没查到继续去进入下一个实现链(查询接口:获取具体缓存值的调用RPC或数据库等)。再看写入redis的逻辑:

    调查询接口接口获取到的value也并没有空等判断,直接设置缓存对象里放入redis里。


    到此,代码上分析整个流程没发现有什么问题,随后开展了本地调试,调用了两次带cache注解的方法,理论上第二次不会去调查询接口了,通过本地调试也的确没有调用查询接口。那就比较奇怪了,为什么本地测试没问题,测试、预发及线上环境都有这个问题。头大了,想了想还是去测试环境试试,然后在切面类中加上日志去测试环境进行测试,发现测试环境并没有打印日志,说明没进入到切面实现类里面,奇怪了,明明加了注解,为什么没进到切面呢,问题肯定还是注解上的问题,回去继续看代码吧:

    发现加cache注解的方法只有本类中的其他方法调用,并没有其他类调用,至此,问题就比较清晰了。


    三、解决思路

          上述问题是通过普通的方法调用方式调用目标方法,切面是不会生效的,因为切面主要应用于通过 Spring AOP 或其他代理机制进行的方法调用。在同一个类中的方法调用不会经过代理,因此切面也不会被触发。可以考虑将目标方法提取到一个单独的类中,并通过依赖注入的方式调用目标方法,以确保切面能够生效。

    经过修改后,已经可以成功缓存结果,日志验证如下:


    四、总结分析

       Java 注解固然可以为我们提供方便,但是需要注意使用场合,不是来个场景就使用注解。下面是一些具体的使用注意事项,供大家参考:

      • 避免循环依赖:不要在注解中使用可能导致循环依赖的类或接口。
      • 不要过度使用注解:注解可以提高代码的可读性和维护性,但过度使用会导致代码复杂。注解适合用于描述简单的元数据信息,对于复杂的业务逻辑,过度使用注解会导致代码难以理解和维护。如果需要在运行时动态修改逻辑,注解并不适合,因为它们在编译时就已经确定了。如果需要根据复杂的条件进行逻辑判断,这种情况下使用注解可能会使代码难以阅读和理解也可能造成频繁修改不利于代码维护。
      • 反射使用:如果需要在运行时通过反射读取注解,确保注解的保留策略至少是RetentionPolicy.RUNTIME。
      • 重写注解:当重写父类方法时,如果父类方法上有注解,需要考虑是否需要在子类方法上也添加相同的注解。
      • 注解继承:注解不会被子类自动继承,如果需要在子类中使用,必须显式添加。
      • 避免重复注解:Java 8 引入了重复注解的概念,但在之前版本中,不能在同一个元素上多次使用同一个注解。
      • 类型检查:在使用注解时,确保类型检查正确,例如,不要将int类型的属性值赋予string类型的注解属性。
      • 性能考虑:运行时注解处理可能会影响性能,尤其是在大量使用反射的情况下。

       遵循上面这些使用注释事项,可以帮助大家更有效地使用Java注解,同时保持代码的清晰和可维护性。


       下面举例一些日常容易出现使用不当的注解:

       @Transactional :当事务注解代码范围的逻辑中有大事务、长事务情况下,可能导致数据库死锁,系统性能极速下降等风险;

       @Async :如过度使用可能导致线程过多内存CPU的使用率增长较大;如代码中有阻塞操作可能导致线程无法释放等风险。

       @自定义注解:当使用自定义注解(例如权限验证、参数转换等),特别是使用三方的自定义注解,一定要先了解其使用范围、使用限制等,避免因不了解而造成线上事故。

       上述举例并非全部的案例,也并未详细展开,后续会再进行一些具体案例的分享,谢谢大家!


       以上仅仅代表个人观点,一点愚见,还请大家批评指正!同时,欢迎大佬们一起来补充!