在项目中是否有遇到过这样的需求?
你负责的项目需要一个功能,但是这个负责实现这个功能却不是你,很可能是第三方厂商给你的一个服务。
那么如何保证插件化呢?
有时候我会想spring是怎么做的,spring如何做到适配不同的数据库连接池,不同的缓存组件呢?
带着疑问,我翻阅了下Spring的源码,关于缓存抽象这部分。
首先我去查看官方文档,
Spring Boot只留下了简短的几行话,首先是开启@EnableCaching,然后更多文档请移步Spring Framework的文档。
行吧,那去Spring Framework文档看下
这里吐槽下,Spring文档的左边大纲不见好久了,每次都不能愉快的翻来翻去了。
文档较长,我这里总结下。
- 讲了缓存是干嘛的
- 讲了@Cacheable @CacheEvict等注解怎么用的
- 讲了缓存的key如何生成
- 讲了条件缓存
- 讲了如何自定义缓存注解
- 讲了JSR-107注解
- 讲了几种缓存的实现组件 ConcurrentHashMap/Caffine等实现
大部分是将如何使用的,那我们试试。
如果只是使用ConcurrentHashMap这种类型的缓存的话,我们甚至都不需要依赖spring-boot-starter-cahce(spring-context-support)这个依赖就可以使用。
依赖如下(Gradle)
这里有一个特别简单的方法, 就是传入一个参数,获取一个字符串.
主方法如下,加上@EnableCaching注解
运行后可以发现,调用了两次方法,但是实际只执行了一次
缓存生效了。
那么现在用的是哪种缓存呢?我们根据官网可以看到有很多种。
我们知道想要生效,指定是经过了某些配置的,那么一定是在autoconfigure包内,于是我们找到了CacheAutoConfiguration.
里面有个方法,往spring容器注册bean定义
于是跟着找到了如下配置
CacheAutoConfiguration导入了这么多的缓存配置,有EnCache的,有Redis的,有CAFFEINE的,真正生效的是哪个呢?
于是我们把所有的配置都打上断点,发现最终是SimpleCacheConfiguration生效
那么问题来了,为什么这么多Configuration,偏偏只是SimpleCacheConfiguration生效呢?更何况按照上面的顺序,SimpleCacheConfiguration甚至还是倒数第二的优先级。
那我们首先肯定是想要这些配置类首先是需要被解析的(不了解的可以看我之前写的spring bean的声明周期),解析Configuration Class, 然后注册bean定义,当然这里注册肯定是有条件的。
那我们现在想要搞明白的是,到底其他的9个配置类有没有注册bean定义到spring容器中呢?
我们现在知道什么?
CacheAutoConfiguration自动配置类,导入了10个Cache的配置。然后只有一个配置类生效。
那我们不妨把断点打到导入配置的部分来,也就是这里
重新启动下,查看调用栈
可以看到,在refresh,调用了invokeBeanFactoryPostProcessors,然后来到ConfigurationClassPostProcessor的processConfigBeanDefinitions()的方法中。
从名字我们就能猜出来,这个解析标注了@Configuration的类的。
然后进入到这个方法中
人家的注释也印证了我们的猜想,这个是解析所有的@Configuration类。
然后执行ConfigurationClassParser的parse方法。
在ConfigurationClassParser 有几个重要的方法:
processImports
processConfigurationClass
doProcessConfigurationClass
首先是解析CacheAutoConfiguration这个类,进入到processImports方法,processImports方法内会调用processConfigurationClass方法,然后调用doProcessConfigurationClass方法,然后会接着调用processImports,然后解析ImportSelector,获得了10个类。
也就是说在解析CacheAutoConfiguration时,加载到了10个CacheConfiguration,然后再递归解析每个配置类。
接下来比较有趣。
会调用一个asSourceClasses方法,因为我们得到的一个类的全限定名,又不是一个真正的Class对象,那我们势必是要转化一波的。
当我们解析到JCacheConfiguration时,发现在
这一步时,永远都会返回一个object对象,这里是因为这一步测试调用了相当于@ConditionalOnClass。
我们看看是怎么做的?
其实特别简单,就是Class.forName调用一下,没抛出异常就说明@ConditionOnClass匹配成功。于是就返回true。
于是上一步就返回了Object对象。
于是根据导入的10个Configuration上面的@ConditinalOnXxx。生成了如下的配置类
接下来我们把断点直接放到
可以看到,在ConfigurationParser解析完毕后,关于Cache的配置类有四个。其中一个是Object类。
然后将这个四个配置注册到bean定义中,当然,首先还是要判断@Conditioanl的,@Conditional 有两个状态 一个是解析Configuration时生效,一个是注册bean定义时。在注册bean定义的时候就用到了@ConditionalOnBean了。
然后在注册bean定义时,过滤掉了GenericCacheConfiguration和NoOpsConfiguration这两个配置类。
所以到最后只有SimpleCacheConfiguration生效了。
以上是简单的缓存实现,使用的ConcurrentHashMap进行缓存。
那我们换成Caffine试一下
这里看出来,应该是有这两个类应该就能生效了。
那我们试试看,如下图,我们加入caffeine的依赖,查看是否真的能够自动配置成功?
我们看到,确实是caffeine的配置生效了。
注意,这里面有个坑!
如果caffeine的版本不对,很可能配置不生效,因为ClassLoader加载类失败,导致OnClassCondition过不去,从而导致caffeine的配置不起作用。
为什么只加入了caffeine的实现,spring就能操作caffeine,就好像你操作其他的缓存实现一样呢?
这就奇了怪了,caffeine又不是Spring自己写的东西,给我们的感觉就像caffeine实现了spring的cache相关的接口似的。
其实我们翻开spring-boot-starter-cache这个依赖,会发现里面只有一个spring-context-support的依赖。
在spring-context-support的依赖内
Spring对caffeine进行了一层包装,使得spring的缓存抽象能够完成像其他缓存一样的操作。
似乎这可以说是 策略模式?适配器模式?装饰者模式?
或者说是防腐层?
似乎都是,你们觉得呢?