声明:本文节选自公众号【看代码再上班】。目录SpringCache的“误用”在哪里?从SpringCache的原理解释为什么私有方法不能缓存从SpringAOP的原理解释为什么私有方法不能缓存SpringCache被“误用”的背景是这样的。同事开发的一个功能模块代码,大概查询一个下游的内容接口,查询数据,转发给端侧接口。这个功能模块的流量非常大,下游内容接口的内容数据量也是有限的(contentid个数有限,量级几w)。同事也意识到需要在自己的服务中为下游的内容接口添加本地缓存。方法和下图一模一样:这段代码发布在网上,打开端侧访问入口后,我们的后台服务开始疯狂报警,都是内容界面未重载的报警压力和响应超时。然后我们先看调用链,发现内容接口的QPS已经飙升到10W了!为什么?很明显,添加了接口缓存。按照我们加缓存的预期,很多请求应该是命中缓存而不是去检查下游的内容接口吧?可能很多人都这么认为,但是错了就是错了。上图中的错误非常“低级”。如果以这种方式使用它们,对于稍微不健壮的下游系统来说将是一场灾难。如果是真的,今年的表现也好不到哪儿去。怎么了?以上代码用于springcache。总结起来有以下错误:1.给私有方法添加缓存2.给类内部方法调用添加缓存。这些问题是比较致命的。我们很多使用缓存的朋友都不知道或者明确这个不能用。下面我就问题一一分析。从SpringCache的原理,解释为什么私有方法不能缓存。SpringCache通过注解,借助SpringAOP实现缓存。打开源码包,定位到我们的@Cacheable注解位置:cache的实现在context的org.springframework.cache包下。我的spring版本是5.3.14,其他版本基本一样。matches方法判断类或方法中是否有缓存相关的缓存注解。这个怎么判断?我们一路跟进,cas.getCacheOperations(method,targetClass)),到了AbstractFallbackCacheOperationSource类的getCacheOperations方法:下次进来的时候不需要消耗cpu重新计算获取。computeCacheOperations方法中,真正解析方法上的缓存注解在findCacheOperations方法中:一路往下,看到SpringCacheAnnotationParser类的parseCacheAnnotations方法时,我们看到spring解析缓存的相关注解并包装注释请参见此处的CacheOperation类。Spring将缓存注解包装在类和方法上,并放入一个集合中。aop方面判断是否有CacheOperation作为入口。那么,这里要重点说明的是springcache做了判断,不支持对非public方法进行缓存注解。逻辑在哪里?细心的你会发现:很明显,不支持非public方法,连protected方法都不支持,更别说private了!这里多说一下。读取缓存的入口代码后,我们很容易找到方法拦截器:我们从execute方法一路跟进,可以看到最后是CacheAspectSupport类的execute方法实现了对缓存的读取对接缓存或更新。如果我们引入三方缓存,比如Caffeine,那么底层使用Caffeine.Cache来存储。想知道如何使用三方缓存工具可以看我的另一篇文章:《人人都说好的Spring Cache!用起来!【文末送书】》结论:springcache明确不支持通过AbstractFallbackCacheOperation#computeCacheOperations方法对非public方法进行注解缓存。从SpringAOP原理解释为什么私有方法不能缓存。上面说了,springcache做了一层限制,不支持带缓存注解的非public方法。那么,springcache为什么要这样做呢?如果单看springcache源码的逻辑,没有这个限制,不也能“穿过去”吗?为了解释这个问题,我们先从SpringAOP的原理说起。我们先调整一下BookService的缓存注解的位置,让方法可以正常运行缓存逻辑:启动我们的spring容器,断点到我们的业务代码bookService.findByBookNameWithSpringCache(bookName):看,BookService引用的是一个Proxy类,这也说明springcache借用了aop的能力。问题来了,为什么是cglib代理呢?在我们的常识中,springaop默认使用java的动态代理,然后使用cglib代理。从spring官网文档也可以证实:em...我没有用接口,所以用了cglib代理。这个解释只对了一半。(因为即使切换到接口实现,还是不能如愿,还是cglib代理,有兴趣的朋友自己试试。)说说为什么一直运行cglib代理。这就是springboot的幽灵。我们的启动类上有一个@SpringBootApplication注解。这是一组组合注释。我们按照这个注解的内部定义找到@EnableAutoConfiguration,然后找到@Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector类的处理方法:里面有很多自动组装的信息,根据AopAutoConfiguration的定义(这个类定义在spring-boot下而不是spring-context下):AopAutoConfiguration类的主要任务是根据配置参数使用注解@EnableAspectJAutoProxy,注释中也说明:启用该类的条件是:配置参数spring.aop.auto的值不为false,在我们的spring-configuration-metadata.json中有配置:AopAutoConfiguration包含以下两个内置的配置类,对应配置参数spring.aop.proxy-target-class=true/false两种情况:spring.aop.proxy-target-class默认配置的时候也是默认为true,我们spring-boot中默认为true。所以默认使用aop的cglib代理。至此,我们就基本知道为什么spring-cache中使用的aop一直使用cglib代理了。说完cglib,终于可以回归正题,“为什么不能在私有方法上使用缓存注解?”如果从aop的角度来分析,那么答案就是:因为cglib。cglib实现了动态代理,其底层使用ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成该类的子类,重写类中所有可以重写的方法。由于cglib的代理类使用了继承,也就是说cglib不能代理final类,同时也不能代理私有方法!子类不能覆盖私有方法!至于cglib是如何生成代理类的,这里就不赘述了。以后有机会专门写一篇文章。这里我们只需要知道springcache的实现使用的是aop函数,aop是不支持private私有方法的。拦截,所以不支持私有方法上的springcache注解。类内部方法调用不支持缓存通过上面的分析,springcahe的缓存功能是由于使用了aop,所以我们可以知道我们的类是经过cglib重新增强和代理的类。如果是类内部的方法调用,为什么不能生效呢?这个问题很简单。我们在内部调用方法的地方打断点,一看就知道:对了,不使用代理怎么用缓存功能呢?结语我是锡,一只努力让自己变得更好的普通攻城狮。我经验有限,知识浅薄。如果您发现文章中有不妥之处,非常欢迎您加我提出建议。我会认真考虑修改的。坚持创作不易,您的好评是我坚持输出最强大的动力,谢谢!终于!???
