近期使用 Spring Cache 所发现问题与部分解决方案

发布于 2020-02-22  3.51k 次阅读


近期在使用Spring Cache期间碰到了一些问题,这里并不会描述Spring Cache的使用,仅仅只是记录问题的场景与解决方式

问题1:

  • 场景:在controller的a方法中调用了service类的某方法(这个方法上加了@Cacheable注解)返回了一个List对象,controller的a方法有一个aop切面,逻辑是将返回的List中插入一个元素,再多次调用后,发现返回的List的元素在逐次递增,表现为调用几次增加几个.
  • 原因:spring cache在使用堆类型缓存存储方案的时候(Caffeine|Guava),缓存对象可以被修改(即返回的对象就是缓存内的对象,并不是复制品)
  • 解决:(仅仅是我的场景适用)在aop处理的时候构造一个新的List对象,并且执行浅拷贝,将原List中的对象加入新的List中,再进行插入一个元素的处理.

问题2:

  • 场景:Spring Cache注解无法在mapper上使用,即在@Mapper标注的接口的方法上使用是无效的

  • 原因:尚未证明是接口问题还是@Mapper注解的问题,但是根据Spring Cache的官方文档中说道:@Cacheable在接口方法上面时如果使用基于类的代理是无效的

    Spring recommends that you only annotate concrete classes (and methods of concrete classes) with the @Cache* annotation, as opposed to annotating interfaces. You certainly can place the @Cache* annotation on an interface (or an interface method), but this works only as you would expect it to if you are using interface-based proxies. The fact that Java annotations are not inherited from interfaces means that if you are using class-based proxies ( proxy-target-class="true") or the weaving-based aspect ( mode="aspectj"), then the caching settings are not recognized by the proxying and weaving infrastructure, and the object will not be wrapped in a caching proxy, which would be decidedly bad.

    这就设计到spring aop动态代理的实现方式了,spring boot默认是cglib,也就是基于类的代理,被代理者必须是类,经过查阅资料,实际上spring 5中动态代理方式是自动转换的.

    Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

    Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. By default, CGLIB is used if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes, business classes normally implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface or where you need to pass a proxied object to a method as a concrete type.

    It is important to grasp the fact that Spring AOP is proxy-based. See Understanding AOP Proxies for a thorough examination of exactly what this implementation detail actually means.

    Spring AOP默认将标准JDK动态代理用于AOP代理。 这使得可以代理任何接口(或一组接口)。

    Spring AOP也可以使用CGLIB代理。 这对于代理类而不是接口是必需的。 默认情况下,如果业务对象未实现接口,则使用CGLIB。

    关于spring aop可以看一下这篇文章https://juejin.im/post/5db7870a518825647178f16c

    那么按道理如果可以自动识别的,那么大概率是@mapper注解问题

  • 解决:这里没有仔细研究,先放个TODO,临时解决方案是将注解移动到service层,而移动到service层则引发了下一个问题

问题3

  • 场景:某个类的a方法调用了同在类里面的b方法(该方法有@Cacheable注解)而这时候Cacheable注解作为切点的aop事件并不会生效,即注解效果失效

  • 原因:由于Spring Cache默认使用proxy mode也就是基于动态代理实现AOP,那么类自身调用是不会经过代理的,这个问题同样适用于@Transactional \ @Async等使用spring aop作为aop实现的注解

  • 解决:使用Aspectj加载时织入(Load-Time Weaving)代替Spring Aop,这个网上资料很少,我指的是Spring Boot aspectj加载时织入的资料

    经过一晚上的google,我找到了跑起来的方法,主要参考

    https://www.credera.com/blog/technology-insights/open-source-technology-insights/aspect-oriented-programming-in-spring-boot-part-3-setting-up-aspectj-load-time-weaving/

    步骤如下:

    • pom中加入依赖:spring-aspects\spring-instrument

    • resources文件夹中新建META-INF并在其中新建aop.xml,这是Aspectj运行所必须的,内容为

    
        //开启日志
            //可以全部扫描
        
    
    • 在启动类加上注解,开启加载时织入:
    @EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
    • spring cache生效注解中加入mode更换的参数,表示启用aspectj模式
    @EnableCaching(mode = AdviceMode.ASPECTJ)
    • JVM启动参数中加入
    -javaagent:/path/to/spring-instrument-{version}.jar -javaagent:/path/to/aspectjweaver-{version}.jar

    以上解决方案仅仅只是表面,希望有相关经验的老哥提供一些探索的思路或者文章以及见解


面向ACG编程