工作中遇到的問題
在使用feign進行微服務間調用的時候,發現用feign無法實作某些業務需求。而Spring Cloud
Ribbon中已經提供了支援負載均衡的RestTemplate,然而RestTemplate的使用比較繁瑣,當然簡單封裝一下也可以,但是畢竟不如feign那樣直接定義一個接口加上注解來得友善。是以我就在想能不能像mybatis或feign那樣自定義一個屬于自己的注解
系列文章
像MyBatis,OpenFeign那樣自定義注解——掃描篇
像MyBatis,OpenFeign那樣自定義注解——Jdk動态代理篇
像MyBatis,OpenFeign那樣自定義注解——CGLib動态代理篇
像MyBatis,OpenFeign那樣自定義注解——SpringAOP實作篇
項目位址:AnnotationDemo
我想要實作的功能
1、定義一個接口
2、在接口上加上自定義注解
3、像feign那樣直接使用
思路
一開始想到的是使用spring aop來實作,實際上用spring aop實作起來也更簡單
但是強迫症的我始終覺得不夠像feign,于是經過對feign源碼的研究,以及自己的思考,我整理了一個實作以上功能的思路
1、自定義一個注解@RestClient用于标記接口類
2、自定義一個注解@EnableRestClient用于指定掃描範圍
3、對掃描出來的接口類進行動态代理
4、注冊成bean
掃描篇所解決的就是思路中的1和2:
掃描出指定範圍内使用@RestClient的接口
先整體的看一下我們會用到那些類:
@RestClient注解——用于标記需要掃描的接口
@EnableRestClient注解——用于指定掃描範圍
RestClientsRegister——用于注冊bean的定義
ClassPathBeanDefinitionScanner——類的掃描器
下面直接上代碼
@RestClient注解——用于标記需要掃描的接口
@Retention(RetentionPolicy.RUNTIME) //運作時保留
@Target(ElementType.TYPE) //注解用在類上
public @interface RestClient {
}
@EnableRestClients 注解——用于指定掃描範圍
@Retention(RetentionPolicy.RUNTIME) //運作時保留注解
@Target(ElementType.TYPE) //用于類上
@Import(RestClientsRegister.class) //這裡是關鍵,這裡告訴了Spring在開始掃描的時候所執行的類
public @interface EnableRestClients {
//指定所掃描的包
String[] basePackages() default {};
}
RestClientsRegister——用于注冊bean的定義
/**
* 實作ImportBeanDefinitionRegistrar用于注冊bean
* 以Aware結尾的接口是标記類,spring會自動裝配對應的bean
*/
public class RestClientsRegister implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
private Environment environment;
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
//spring先從所有注解中找到我們所定義的@EnableRestService
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableRestClients.class.getName());
//擷取@EnableRestService中的basePackages參數
String[] packages = (String[]) annotationAttributes.get("basePackages");
if (packages.length == 0) {
//如果沒有指定目錄,以根目錄為目錄
String className = importingClassMetadata.getClassName();
String basePath = className.substring(0, className.lastIndexOf("."));
packages = new String[]{basePath};
}
/*建立掃描器,重寫isCandidateComponent方法
*isCandidateComponent傳回該類是否是候選類
*/
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(
registry, false, environment, resourceLoader) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
/*
* beanDefinition.getMetadata()包含掃描到的類的資訊
* 這裡隻有接口才符合我的要求
*/
if (!beanDefinition.getMetadata().isInterface()) {
System.out.println(beanDefinition.getMetadata().getClassName() + "不是接口");
return false;
}
return true;
}
};
//添加掃描過濾器,這裡指掃描含有@RestClient注解的類
scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class));
/* 調用scanner.findCandidateComponents()開始掃描候選類
* 掃描指定包路徑下的所有包含@RestClient注解的類
* 交給ClassPathBeanDefinitionScanner.isCandidateComponent()方法判斷是否是候選類
*
* 這裡使用了java8 stream方法,
* 其實就是周遊packages,把所有候選類放在一個set裡面
* */
Set<BeanDefinition> beanDefinitions = Arrays.stream(packages)
.map(scanner::findCandidateComponents)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
//周遊掃描出來的候選類
for (BeanDefinition beanDefinition : beanDefinitions) {
registerBean(beanDefinition.getBeanClassName());
}
}
/**
* 代理接口并注冊
*
* @param className 類名
*/
public void registerBean(String className) {
//todo 待實作
System.out.println("找到了一個接口:" + className);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
這樣一來,注解的掃描的工作就完成了,接下來我們将@EnableRestClients注解到啟動類上
@SpringBootApplication
@EnableRestService
public class CloudFileApplication {
public static void main(String[] args) {
SpringApplication.run(CloudFileApplication.class, args);
}
将@RestClient注解到一個接口和一個類上
@RestClient
public class UserInfo {
}
@RestClient
public interface UserMessage {
}
運作結果:

可以看到,UserInfo是一個類,而UserMessage是一個接口,通過重寫ClassPathBeanDefinitionScanner類的isCandidateComponent方法我們可以自行決定哪些類可以作為候選類。
至此,類的掃描工作就完成了
現在我們得到了通過@RestClient注解的接口
在RestClientsRegister.registerBean()方法中我隻列印了類名
在下一篇文章中我們将通過動态代理的方式代理接口,并且注冊成bean