本文使用的
spring-boot
版本是2.0.2.RELEASE
,本文力求白话,不会引入大段晦涩的框架代码造成阅读的极度不适,但是本文会引入一定量阅读舒适的代码,而且,读者需要有一定的框架基础。
1. 故事起源
最近在研究Spring中的缓存机制。就拿我自己的项目来讲,最早用了EhCache,后来全部迁移至Redis。我们知道在SpringBoot中开启Redis作为缓存是比较方便的,直接引入maven依赖即可:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
就这么简单,不需要任何多余的代码行,SpringBoot就帮我们做完了一切的集成配置。我们来一段测试代码:
这里注意需要打开@EnableCaching
功能,否则@Cacheable
注解的方法不会被cglib
代理。
CacheApplication.java
@EnableCaching
@SpringBootApplication
public class CacheApplication {
@Autowired
private CacheService cacheService;
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(CacheApplication.class, args);
CacheApplication app = ctx.getBean(CacheApplication.class);
System.out.println(app.cacheService.getStr(UUID.randomUUID().toString()));
}
}
CacheService.java
@Service
public class CacheService {
@Cacheable(cacheNames="XA", sync=true)
public String getStr(String key) {
return "value";
}
}
跑一下,然后在redis中执行 keys *
,结果为:
127.0.0.1:6379> keys *
1) "XA::f17ecb4d-b0e3-4bdc-9973-7592e9c7bacc"
OK,引入缓存成功!
2. AutoConfiguration初探
本来故事讲完了该结束了,不过作为一名有理想,有道德,有文化的攻城狮,怎么能不去了解博大精深的SpringBoot是如何帮我们引入这些自动配置的呢?
作为一个SpringBoot的资深新手,我们知道SpringBoot的傻瓜式懒人配置都存在于spring-boot-autoconfigure
包中,那么我们就去这个包里寻找下线索。
不看不知道,一看居然有这么多种类型的缓存配置,Spring果然是包罗万象,主流的非主流的什么都有。
粗略的看了一下这些配置文件,大部分都有引入@Conditional
的条件,比如EhCacheCacheConfiguration
就需要有EhCache包提供的net.sf.ehcache.Cache
定义才可以生效,而我们在上面的代码中没有引入EhCache包,自然不会生效。而RedisCacheConfiguration
则需要一系列spring-boot-data-redis
包提供的组件才可以生效,我们引入了,所以自然就会去使用Redis作为缓存
EhCacheCacheConfiguration.java
@Configuration
@ConditionalOnClass({ Cache.class, EhCacheCacheManager.class })
@ConditionalOnMissingBean(org.springframework.cache.CacheManager.class)
@Conditional({ CacheCondition.class,EhCacheCacheConfiguration.ConfigAvailableCondition.class })
RedisCacheConfiguration.java
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
正是通过这些条件的限制,SpringBoot才会根据引入的pom包选择合适的缓存配置类。
3. 特殊的配置类
故事到这里结束了?没有,仔细看一看这些配置类,发现有几个特殊的小哥
NoOpCacheConfiguration
SimpleCacheConfiguration
你问我哪里特殊了?确实很特殊,因为他们两个的生效条件都是满足的,也就是说,这两个都能作为缓存配置引入,但是SpringBoot却非常“智能”的用了Redis作为缓存。
NoOpCacheConfiguration
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
SimpleCacheConfiguration
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
什么时候SpringBoot有AI功能了?囧,这不可能!
必须不可能啊,所以SpringBoot肯定做了一些事情,那么我们就再仔细探究下为什么只有RedisCacheConfiguration
被spring-boot
选中了,其他两个却落榜了。
4. 再次探究选择机制
我们知道,SpringBoot在初始化的时候,会去spring-boot-autoconfigure
包中寻找配置类。因为SpringBoot的设计理念就是即插即用,傻瓜式,让开发者告别繁琐的配置,所以这个包中内容是很丰富的,基本上主流的中间件都会在这个包中留下自己的配置定义。但是spring在初始化的时候并不是将所有的配置类都会加载进来,这时候那些没在spring-boot-autoconfigure
包中的spring.factories
中定义的,那些不满足@Conditional
的的配置,就会被SpringBoot过滤掉。所以,如我们上面的测试代码,其实只引入了Redis部分的配置。
但是,这两个特殊的缓存配置类却是满足导入要求的,因此,我们在查看configClasses
集合包含的类定义的时候,确实是有这两个配置类的定义的:
ConfigurationClassPostProcessor.processConfigBeanDefinitions 方法中的 configClasses
到这里暂时还没思绪,问题还是SpringBoot为何选择的是Redis缓存配置,而不是选择其他缓存配置。带着这个问题再次阅读一遍三个可选的缓存配置的导入条件。我们发现
RedisCacheConfiguration, NoOpCacheConfiguration, SimpleCacheConfiguration
的条件都有一个是@ConditionalOnMissingBean(CacheManager.class)
并且我们看到三个配置类中都有对这个CacheManager
的Bean输出。那么,这个问题的思路可能就变成这样了,肯定是三个配置类中的其中之一个抢跑了,输出了CacheManager
的实例Bean定义,另外两个就导致判断@ConditionalOnMissingBean(CacheManager.class)
条件失败而被废弃了。
5. 真相大白
到这里有了基本的思路,一定是SpringBoot根据某种条件对这三个配置类的加载顺序做了定义,现在我们需要做的是看这三个配置类的加载先后顺序SpringBoot究竟是怎么定义的。
在初始化的时候,SpringBoot首先会去寻找用户项目代码中是否包含@EnableAutoConfiguration
注解,其实这个注解默认情况下已经被@SpringBootApplication
所包含,找到之后会处理@EnableAutoConfiguration
注解所导入的AutoConfigurationImportSelector
类,这个类会根据spring-boot-autoconfigure
包中预定义的spring.factories
文件中去搜寻包中符合条件的配置类导入。这其中,包含了spring.factories
列举的配置之一org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
。
我们看到这个CacheAutoConfiguration
配置类中也有一个@Import
定义,我们打开它的定义查看一下,他包含一个相当长的注解列表:
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
查看这个CacheConfigurationImportSelector
类,发现如下代码
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
这个CacheType
就立刻映入眼帘了,查看一下发现是一个枚举,到这里就明白了,在这个枚举里,定义了配置类的加载顺序,我们看到了,REDIS
是排在SIMPLE
和NONE
之前,这样子,优先被载入的肯定是Redis的配置类也就是RedisCacheConfiguration
。那么载入RedisCacheConfiguration
之后呢?RedisCacheConfiguration
有一个CacheManager的Bean
导出定义,那么,在RedisCacheConfiguration
加载之后,另外两个缓存配置类的加载条件就不满足@ConditionalOnMissingBean(CacheManager.class)
条件,自然被Spring所抛弃了。
6. 总结
到这里我们分析了SpringBoot对缓存加载选择的内部处理流程,SpringBoot由于支持多元化的缓存方案,因此在配置的时候,需要对这一块有所了解,防止掉坑。
网友评论