淺談
編寫目的
- 學習一下 @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 是怎麼設計的。