“无感”就是最棒的设计
问题
想直接解决问题不愿听故事的,进屋坐坐看第二节。
最近在公司领到一个任务,优化公司的缓存框架的使用体验。公司的缓存框架封装成一个JAR文件,提供基于注解的对几种缓存的一致性使用。覆盖的缓存方案有ehcache
memcached
redis
,先向公司大牛致敬。
要使用缓存框架,需要做这么几件事:
引入缓存框架的JAR文件
引入想要使用的缓存JAR,比如memcached.jar
做相关的Spring Bean配置,比如要使用Memcached缓存就要有如下的配置:
<!-- memcache配置示例 --> <bean name="ljmemcachedClient" class="net.rubyeye.xmemcached.utils.XMemcachedClientFactoryBean" destroy-method="shutdown"> <!-- 多个地址空格分隔 --> <property name="servers" value="${cache.memcacheAddress}" /> <!-- 连接池大小一般5已经足够用,根据项目组实际情况调整 --> <property name="connectionPoolSize" value="${cache.memcachePoolSize}" /> <!-- 一致哈希分布 --> <property name="sessionLocator"> <bean class="net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator"></bean> </property> <!-- 二进制协议,提高数据传输效率,支持touch等 --> <property name="commandFactory"> <bean class="net.rubyeye.xmemcached.command.BinaryCommandFactory"></bean> </property> </bean>
必要的配置文件:
cache.memcacheAddress=10.8.1.107:11211 cache.memcachePoolSize=1
如果要使用redis缓存,则要引入另外一套配置。每个新项目都要配置,这显然是令人沮丧的。能不能简化这些配置呢?
- 方案一:在缓存框架中引入三种缓存的JAR包,并做三种缓存的
Bean
配置,不提供配置文件,由使用缓存框架的项目提供。 - 方案二:在缓存框架中提供三种缓存的“自动配置”,由使用缓存框架的项目提供配置文件和相关缓存JAR包,根据引入的缓存JAR包“自动配置”适合的
Bean
对象,完成缓存的使用。
来看下两种方案的优缺:
- 方案一
- 优点:简单易实现
- 缺点:JAR包臃肿,无论实际使用哪种缓存,都引入了三种缓存JAR,并且需要提供三种的配置文件。不符合程序员美学–简约
- 方案二:
- 优点:“智能”发现缓存,按需引入,语义明确,使用方便
- 缺点:程序实现复杂
我选择方案二。
然而Spring Boot 已经有成熟的自动配置实现方案,如果直接引入 spring-boot-autoconfigure.jar 会面临额外问题:
- spring-boot-autoconfigure.jar 没有“直接”提供针对
memcahced
的自动配置; - spring-boot-autoconfigure.jar 提供了太多自动配置支持,而我只想要关于缓存的几个;
那就只好模仿Spring Boot搞一搞了。
终焉
撒花…完结
在开始赘述之前,先给出最终的成品。
如何使用自动配置?
引入自动配置JAR包
使用注解
@EnableAutoConfigure
开启自动配置。建议写在基础类上或者专门提供一个类,如:@EnableAutoConfigure public class AutoConfigure { }
根据需要选择具体缓存,引入JAR并给出配置信息。比如memcached.jar
cache.memcached.servers=10.8.1.107:11211 cache.memcached.connectionPoolSize=2
PS: 这里有一个纠结的地方,其实可以做到省略第2步,也就是在自动配置JAR包里把这事做了。但笔者感觉这样不好,好像是降低了使用的成本,但是损失了使用的自由。所以最终还是没有进一步封装。这里仅代表笔者个人感受,未必是对的。
PS2:然而什么是对的呢?世界本来就不是0和1.
赘述
接下来详细说说如何实现的。
有条件的创建
除非满足条件,不然。。。哼!
最本质的需求是根据条件加载Bean。 Spring4
提供了实现方案 — @Conditional
,可以通过条件判断创建 Bean
。
示例:
// java config
@Configuration
public class TestBeanConfig {
// 根据条件创建, 条件写在TestConditional类里
@Bean
@Conditional(TestConditional.class)
public TestBean createTestBean() {
return new TestBean();
}
}
// 配套的条件类实现
public class TestConditional implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
只有当 TestConditional.matches()
结果为 true 时才会创建 TestBean
。
注解 @Conditional
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Conditional {
Class<? extends Condition>[] value();
}
- 作用范围: 类,方法
- 包含参数: value, 接口
Condition
的实现类数组。数组内所有的条件都要满足哟!
如果value中的条件都满足,就创建接下来的Bean。没啥好说的了;
接口 Condition
public interface Condition {
// 当返回值为true时,创建Bean;否则忽略。
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
接口很简单只有一个matches方法,可以通过context和metadata获得相应判断信息,辅助判断;
参数 ConditionContext
public interface ConditionContext {
// 检查Bean定义
BeanDefinitionRegistry getRegistry();
// 检查Bean是否存在,甚至探查Bean的属性
ConfigurableListableBeanFactory getBeanFactory();
// 检查环境变量是否存在以及它的值是什么
Environment getEnvironment();
// 检查加载的资源
ResourceLoader getResourceLoader();
// 加载并检查类是否存在
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}
- 借助
isAnnotated()
能够判断带有@Bean
注解的方法是不是还有其他特定注解; - 借助其他方法可以检查
@Bean
注解的方法上其他注解的属性;
尝试自动配置
判断条件
至此,基础技能已经 GET ,要解决第一节的问题,只要做判断:当前类路径下是否存在某类,有就创建无则忽略。
Spring Boot 在 @Conditional
基础上扩展了几个注解方便做此类判断:@ConditionalOnClass
, @ConditionalOnMissingClass
, @ConditionalOnBean
, @ConditionalOnMissingBean
;
这里就不贴源码了,想看的朋友去这里 org.springframework.boot.autoconfigure.condition;
当然可以自行编写条件类实现判断,我这里就懒省事了,直接使用 Spring Boot 的实现。(才没有这么简单)
编写自动配置类
编写 memcached
和 redis
的Java Config类。核心代码:
/**
* memcached 缓存自动配置
* 在引入googlecode-xmemcached.jar时启用
* Created by mw4157 on 16/7/6.
*/
@Configuration
@ConditionalOnClass(MemcachedClient.class)
public class MemcachedAutoConfiguration {
/**
* 创建一个工厂Bean
*/
@Bean
@ConditionalOnMissingBean
public XMemcachedClientFactoryBean memcachedClientFactory() {
return ...;
}
}
/**
* redis 自动配置
* 在引入redis-clients.jar和spring-data-redis.jar时启用
* Created by mw4157 on 16/7/8.
*/
@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
public class RedisAutoConfiguration {
/**
* Redis 连接配置
*/
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
return ...;
}
}
代码的核心功能是创造需要的Bean,同时辅以@Conditional
相关的注解,就可以在大意上解决问题。
赘述2.0
之所以说大意上,是因为还有其他内容没有注