浅谈
编写目的
- 学习一下 @EnableXXX 在 SpringBoot 中是如何做到可插拔的
- 学习在 starter 中使用代理方式来解析自定义注解并进行方法增强
- 网上 starter 案例千篇一律,感觉都是复制粘贴,没有找到一篇有价值的文章。只好逼迫自己看源码学习。
带来收获
- 了解@EnableXXX注解是如何起作用的
- 了解 AOP 是如何做到方法增强
- 了解如何基于 SpringBoot 编写一个自定义 starter
源码地址
GitHub 地址:
https://github.com/LuckyToMeet-Dian-N/cache-spring-boot-starter
Gitee 地址
https://gitee.com/reway_wen/cache-spring-boot-starter
框架介绍
- 缓存框架目前支持注解方式进行对象缓存
- 缓存方面目前仅支持本地内存存储,多级缓存待支持
- 暂不支持过期策略。
使用方式
打包项目
下载源代码,将代码 install 到本地仓库中,在 SpringBoot 项目中引入即可。
<dependency>
<groupId>com.gentle.cache</groupId>
<artifactId>cache-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
如何使用框架
启用缓存
@EnableCacheManager 注解开启缓存支持。
@SpringBootApplication
@EnableCacheManager //启用缓存
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
测试Controller
支持的注解:
@PutCache(用于放入缓存与读取缓存)
@RemoveCache(用于移除缓存)
使用过程中多个参数中间使用下划线分隔,如果参数参数不存在,则会打印异常提示。
@RestController
public class TestCache {
@GetMapping(value = "test1")
@PutCache(key = "abc")
public String test1(String abc){
return "aaa";
}
@GetMapping(value = "test2")
@PutCache(key = "users.id")
public Users test2(Users users){
return users;
}
@GetMapping(value = "test3")
@PutCache(key = "name_id")
public String test3(String name,Integer id){
return name+" "+id;
}
@GetMapping(value = "test4")
@PutCache(key = "users.id_users.name_test")
public String test4(Users users,Integer test){
return users.toString()+" "+test;
}
@GetMapping(value = "test5")
@RemoveCache(key = "users.id_users.name_test")
public String test5(Users users,Integer test){
return users.toString()+" "+test;
}
}
@Data
public class Users {
private String name;
private Integer id;
}
源码解析
开启缓存
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
//缓存配置选择器,这里是核心。加载是从此处开始
@Import(CacheConfigurationSelector.class)
public @interface EnableCacheManager {
/**
* 优先级
* @return 级别
*/
int order() default Ordered.LOWEST_PRECEDENCE;
/**
* 代理模式
* @return
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* 存储类型
* @return
*/
ExecuteTypeEnum type() default ExecuteTypeEnum.MEMORY;
/**
* 是否启用异步更新缓存
* @return
*/
boolean asyn() default false;
/**
* 代理 class,默认为动态代理,
* 为 true 需要引入 aspectJ 包
* 目前框架不支持
* @return
*/
boolean proxyTargetClass() default false;
}
缓存配置装载
SpringBoot 中导入需要装载 @EnableXXX 注解的配置类需要继承 AdviceModeImportSelector,在 selectImports 方法中选择需要导入的类的全限定名称。
public class CacheConfigurationSelector extends AdviceModeImportSelector<EnableCacheManager> {
private String[] getProxyImports() {
List<String> result = new ArrayList<>(3);
//自动代理注册,Spring 已经提供
result.add(AutoProxyRegistrar.class.getName());
//框架核心配置类
result.add(ProxyCacheConfiguration.class.getName());
return StringUtils.toStringArray(result);
}
@Override
protected String[] selectImports(AdviceMode adviceMode) {
//默认模式是动态代理,目前不支持 ASPECTJ
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return null;
default:
return null;
}
}
}
核心配置类
ProxyCacheConfiguration是我们框架整个核心配置类,其中创建的拦截器、advisor、 参数解析器、注解解析器等对象交由 Spring 管理。
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Configuration
public class ProxyCacheConfiguration extends AbstractCacheConfiguration {
/**
* 通知,AOP 实现方式之一。
* 需要放入拦截器等信息
* @return BeanFactoryCacheInstacneAdvisor
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheInstacneAdvisor cacheAdvisor() {
BeanFactoryCacheInstacneAdvisor advisor = new BeanFactoryCacheInstacneAdvisor();
//放入拦截器
advisor.setAdvice(createBeanRouteInterceptor());
//获取 EnableCacheManager 中配置的全局信息
//此处设计不好,待改善。CacheContext 对外暴露了
if (Objects.nonNull(this.enableCaching)) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
CacheContext.STORE_TYPE = this.enableCaching.getEnum("type");
CacheContext.ASYN = this.enableCaching.getBoolean("asyn");
}
return advisor;
}
/**
* 注解解析器,解析注解中配置的key、prefix、time 等信息
* @return CacheOperationSource
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource createCacheOperationSource() {
return new AnnotationCacheOperationSource();
}
/**
* 参数解析器,根据用户提供的key,解析得到具体传入值
* 将值拼装成缓存 key
* @return ParameterResolver
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ParameterResolver createParameterResolver() {
return new LocalParameterResolver();
}
/**
* 拦截器,拦截被自定义注解标记的方法
* @return CacheInterceptor
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor createBeanRouteInterceptor() {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.setCacheOperationSource(createCacheOperationSource());
interceptor.setParameterResolver(createParameterResolver());
return interceptor;
}
}
切点切面
核心切面以及切点类,CacheChooseSourcePointcut 中的 matches方法告知遇到我们自定义注解时,要进行增强。
public class BeanFactoryCacheInstanceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
private CacheChooseSourcePointcut pointcut = new CacheChooseSourcePointcut();
@Override
public Pointcut getPointcut() {
return pointcut;
}
}
// 切点
public class CacheChooseSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
@Override
public boolean matches(Method method, Class<?> aClass) {
return method.isAnnotationPresent(PutCache.class)||method.isAnnotationPresent(RemoveCache.class);
}
}
拦截器
类实现MethodInterceptor接口,invoke可以理解为JDK中反射中的Method.invoke调用原来方法,可以在该方法前后进行增强其他操作。比如打印日志等。下面逻辑较简单,不做阐述。
public class CacheInterceptor extends AbstractCacheSupport implements MethodInterceptor, Serializable {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Collection<CacheOperation> cacheOperations = getCacheOperationSource().getCacheOperations(invocation.getMethod(), invocation.getMethod().getClass());
Object o = postProcessBefore(invocation.getMethod(), invocation.getArguments(), cacheOperations);
if (Objects.nonNull(o)) {
return o;
}
Object proceed = invocation.proceed();
postProcessAfter(proceed, invocation.getMethod(), invocation.getArguments(), cacheOperations);
return proceed;
}
@Override
protected Object postProcessBefore(Method method, Object[] args, Collection<CacheOperation> cacheOperations) {
CacheOperation cacheOperation = cacheOperations.stream().filter(operation -> operation.getType().equals(0)).findAny().orElse(null);
if (Objects.isNull(cacheOperation)) {
return null;
}
String cacheKey = getCacheKey(method, args, cacheOperation);
return get(cacheKey, method.getReturnType());
}
@Override
protected void postProcessAfter(Object value, Method method, Object[] args, Collection<CacheOperation> cacheOperations) {
cacheOperations.forEach(cacheOperation -> {
String cacheKey = getCacheKey(method, args, cacheOperation);
switch (cacheOperation.getType()) {
case 0:
put(cacheKey, value, cacheOperation.getTime());
break;
case 1:
remove(cacheKey);
break;
default:
break;
}
});
}
}
注解解析器
注解解析器接口如下,主要将我们加在某个方法的缓存注解的信息转为对象,便于后续使用。代码较多,此处不放代码。
public interface AnnotationParser {
@Nullable
Collection<CacheOperation> parseCacheAnnotations(Method method);
}
参数解析器
参数解析器目的是将注解解析器解析得到的对象和方法传入参数进行匹配,匹配后以传入值方式拼接为缓存存储的 key。代码较多,此处不放代码。
public interface ParameterResolver {
String resolverParameter(Method method, Object[] args,CacheOperation cacheOperation);
}
最后
按照当前思路,是可以完成一个基于SpringBoot 的 starter 来编写自定义注解,如果你了解了基本流程,那么SpringBoot 提供的大部分 @EnableXXX 的注解实现有了个基本概念,接着可以深入了解Spring 是怎么设计的。