天天看點

像MyBatis,OpenFeign那樣自定義注解——掃描篇掃描篇所解決的就是思路中的1和2:

工作中遇到的問題

在使用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 {
}
           

運作結果:

像MyBatis,OpenFeign那樣自定義注解——掃描篇掃描篇所解決的就是思路中的1和2:

可以看到,UserInfo是一個類,而UserMessage是一個接口,通過重寫ClassPathBeanDefinitionScanner類的isCandidateComponent方法我們可以自行決定哪些類可以作為候選類。

至此,類的掃描工作就完成了

現在我們得到了通過@RestClient注解的接口

在RestClientsRegister.registerBean()方法中我隻列印了類名

在下一篇文章中我們将通過動态代理的方式代理接口,并且注冊成bean