深刻理解Spring IOC之擴展篇(五)、基於註解整合Spring框架

本篇講的是如何將咱們本身的業務邏輯和Spring框架整合起來,整合的方式主要採用的是註解,裏面涉及到了多個知識點。java

咱們的目的是作出咱們本身的註解,主要是標在接口上,當調用接口裏相應的方法的時候,就會執行咱們本身的邏輯。面試

對的,就是如今的MyBatis和Feign的整合方式,這種也是如今比較容易的,若是你業務裏面xml用的多,你也能夠結合xml來搞,拓展xml的文章我以前已經說過,你能夠翻回去看看。 bash

咱們須要作不少步工做,咱們把這些步驟拆開了一步一步來作app

一、自定義註解:

首先固然是須要自定義出咱們本身的註解框架

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnno {

}
複製代碼

我簡單的說一下上面四個註解的意思哈,@Target表述註解能夠被標註的地方,ElementType.TYPE表示只能被標註在類上。@Retention表示的是註解的生命週期,這裏的RententionPolicy.Runtime表示它在被加載到jvm中以後還依然存在。@Documented表示這個註解會被javadoc工具所記錄。@Inherited表示這個註解是會被繼承的,其實也就是當A有咱們這個@MyAnno的時候,B繼承了A,B也會擁有這個註解而已。jvm

畫外音:我沒有將@Target以及@Retention中全部的值都拿出來說,由於那樣的話我文章就寫不完了,並且這也不是咱們本章的重點,你們能夠自行了解一下這個~ ide

咱們有了本身的註解以後,還須要讓Spring能夠識別的來咱們的註解,那麼此時咱們須要擴展我以前講過的BeanDefinitionRegistryPostProcessor,代碼以下:工具

@Component
public class MyAnnoConfigurationPostProcessor1 implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {

    private String[] basePackages;

    public MyAnnoConfigurationPostProcessor1() {
        // 暫且寫死掃描路徑
        this.basePackages = new String[]{"com.example.demo.external5"};
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 1. new 一個ClassPathBeanDefinitionScanner對象,ClassPathBeanDefinitionScanner這個玩意是
        // Spring 默認的註解掃描器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
        // 2.爲上面呢建立好的scanner對象添加Filter,主要目的是讓它可以認識咱們的註解
        scanner.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
        // 3.進行掃描
        scanner.scan(this.basePackages);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {}
}
複製代碼

其實你也可使用ComonentScan的include屬性,這樣會來的更簡單一些。可是我爲了劇情的進一步發展,就先引出BeanDefinitionRegistryPostProcessor post

而後咱們把咱們的註解找個類標上:測試

@MyAnno
public class Person {

}
複製代碼

接着用這段代碼測試下:

public class Test {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Config.class);
        Person p = annotationConfigApplicationContext.getBean(Person.class);
        System.out.println(p);
    }
}

// 配置類,實際做用就是上面的ComponentScan註解
@ComponentScan(basePackages = "com.example.demo.external5")
public class Config {
}
複製代碼

咱們能夠看到以下的測試結果:

此時咱們便擁有了一個本身定義的註解,這個註解如今和Spring本來的這四個註解@Component、@Controller、@Repository、@Service的做用是同樣的,而且,它如今仍是對接口無效的,由於標在接口上的話,會被Spring的註解掃描器ClassPathBeanDefinitionScanner這玩意忽略掉,所以咱們接下來須要本身定義咱們本身的註解掃描器。

二、自定義註解掃描器

注意:咱們自定義的註解掃描器須要有掃描接口的功能,咱們先來簡單的實現一下它

public class ClassPathAnnoScanner extends ClassPathBeanDefinitionScanner {

    // 必須有這樣一個構造方法,否則報錯,由於父類沒有無參構造,這是因爲java的繼承機制致使的
    public ClassPathAnnoScanner(BeanDefinitionRegistry registry) {
        super(registry);
        // 在構造器中就加上filter,使它天生就能夠認識咱們的自定義註解
        this.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
    }

    // 這個方法是使掃描器可以掃描註解的核心
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 調用父類方法的掃描功能,返回BeanDefinition
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

        if (beanDefinitions.isEmpty()) {
            System.out.println("掃描到的 beanDefinitions 是空的,沒法進行進一步操做!");
        }

        return beanDefinitions;
    }
}
複製代碼

而後在代碼中結合BeanDefinitionRegistryPostProcessor去使用:

@Component
public class MyAnnoConfigurationPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {

    private String[] basePackages;

    public MyAnnoConfigurationPostProcessor() {
        // 暫且寫死
        this.basePackages = new String[]{"com.example.demo.external5"};
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 1. 使用咱們本身的掃描器
        ClassPathAnnoScanner scanner = new ClassPathAnnoScanner(registry);
        // 2.爲上面呢建立好的scanner對象添加Filter,主要目的是讓它可以認識咱們的註解
        scanner.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
        // 3.進行掃描
        scanner.scan(this.basePackages);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // 空實現便可
    }
}
複製代碼

這時候咱們即可以把咱們的MyAnno註解標在接口上了,可是,若是你此時把它標在接口上面而且啓動的話,那是會報錯滴,緣由也很簡單哈,接口是沒有構造方法的,因此沒法初始化。咱們若是想像MyBatis或者是Feign那樣子在咱們調用一個接口的方法以後能夠執行相應的邏輯的話,須要在運行時期生成一個相應接口的代理,而且這個代理還須要藉助FactoryBean來生成(末尾含FactoryBean面試題)。

可是具體是怎麼作的呢?其實仍是得繼續改造咱們的註解掃描器,咱們來看看改造的代碼:

public class ClassPathAnnoScanner extends ClassPathBeanDefinitionScanner {

    // 必須有這樣一個構造方法,否則報錯,由於父類沒有無參構造,這是因爲java的繼承機制致使的
    public ClassPathAnnoScanner(BeanDefinitionRegistry registry) {
        super(registry);
        this.addIncludeFilter(new AnnotationTypeFilter(MyAnno.class));
    }

    // 這個方法是使掃描器可以掃描註解的核心
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

        if (beanDefinitions.isEmpty()) {
            System.out.println("掃描到的 beanDefinitions 是空的,沒法進行進一步操做!");
        } else {
            // 調用修改BeanDefinition的方法
            processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }

    // 相比上面,多了個這個修改BeanDefinition的方法
    // 須要在這裏把interface的beanClass轉爲特定的beanClass
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();

            // 這個會使Spring優先選擇對應的有參構造
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            // 把掃描到的interface改成FactoryBean,這樣便能以FactoryBean的方式初始化
            beanDefinition.setBeanClassName(MyFactoryBean.class.getName());
        }
    }
}
複製代碼

能夠看到哈,相比以前的,咱們還須要去修改掃描到的BeanDefinition,否則你讓Spring給你初始化接口,Spring是會讓你嗝屁的。

三、用FactoryBean建立代理

在用Factory建立代理以前,你首先要知道代理是怎麼建立的,若是這都不知道的話麻煩自行百度jdk的動態代理。

首先咱們先建立出咱們的代理的處理器邏輯

// 這個是屬於jdk動態代理的東西
public class MyServiceProxy implements InvocationHandler {
    
    private Class target;

    public MyServiceProxy(Class target) {
        this.target = target;
    }

    public Object getProxy() {
        // 建立代理的核心邏輯
        return Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 每次被代理的接口的方法被調用就會走到這裏來,在這裏咱們能夠作不少事情
        // 我這裏只是簡單打印了方法的全路徑名稱而已
        // 你能夠在這裏根據每一個方法的名稱不一樣作不一樣的事情,甚至還能夠根據方法參數裏的method去拿方法的註解,獲取註解的信息,進而作更多的事情
        System.out.println(method.getDeclaringClass().getName() + "." + method.getName());
        return null;
    }
}
複製代碼

而後再實現咱們本身的FactoryBean:

public class MyFactoryBean<T> implements FactoryBean<T> {
    // 必須是接口的class
    private Class<T> clazz;

    public MyFactoryBean() {
    }

    public MyFactoryBean(Class<T> clazz) {
        this.clazz = clazz;
    }


    @Override
    public T getObject() throws Exception {
        // 建立代理
        return (T) new MyServiceProxy(clazz).getProxy();
    }

    @Override
    public Class<?> getObjectType() {
        return clazz;
    }

    public void setClass(Class<T> clazz){
        this.clazz = clazz;
    }
}
複製代碼

在這裏,先說一下FactoryBean的機制。FactoryBean這個也是用於建立對象的,若是咱們某個類好比A.java實現了FactoryBean的話,而且你給這個類標上了@Component這樣的註解,那麼,當調用getBean("a")的時候,咱們獲取到的是FactoryBean中getObject返回的對象。若是咱們想要獲得FactoryBean自己應該則應該調用在參數前加上"&",好比getBean("&a")這樣去調用,或者getBean(A.class)這樣傳個Class對象進去也能夠,具體的緣由能夠看我前面的文章。

此時咱們能夠嘗試着把咱們的@MyAnno註解加在接口上:

@MyAnno
public interface TestService {

    void eat();

}
複製代碼

接口很簡單,也沒有實現類。而後用以下測試代碼進行測試

public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Config.class);
        TestService testService = (TestService) annotationConfigApplicationContext.getBean("testService");
        testService.eat();
    }
複製代碼

完了能夠看到控制檯輸出的內容以下:

此時還沒完,由於別忘了,咱們的MyAnnoConfigurationPostProcessor裏面的掃描路徑是寫死的。通常來講,咱們會把掃描路徑配合一個註解寫到啓動類上,方便統一管理,就像Mybatis的@MapperScan那樣。明確目標後,而後咱們來解決咱們的問題

四、使用註解配置掃描路徑

咱們定義一下咱們本身的Scanner:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyAnnoScannerRegistrar.class)
public @interface MyAnnoScanner {
    
    // value 爲包掃描的路徑
    String[] value() default {};
}
複製代碼

注意這個import註解引進來的這個class是重中之重,它是用來對咱們這個MyAnnoScanner裏面value值對應的包來進行掃描的,咱們來看看代碼:

// 必須實現ImportBeanDefinitionRegistrar
public class MyAnnoScannerRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 獲取被Import註解所標着的類的元數據
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyAnnoScanner.class.getName()));
        List<String> basePackages = new ArrayList();
        // 獲取MyAnnoScanner裏面的路徑
        for (String pkg : annoAttrs.getStringArray("value")) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }
        ClassPathAnnoScanner scanner = new ClassPathAnnoScanner(registry);
        for (String basePackage : basePackages) {
            // 針對每一個路徑進行掃描
            scanner.doScan(basePackage);
        }
    }
}
複製代碼

最後咱們在咱們的Config.java上加上咱們的註解:

@ComponentScan(basePackages = "com.example.demo.external5")
@MyAnnoScanner(value = "com.example.demo.external5")
public class Config {
}
複製代碼

注意哈,以前的MyAnnoConfigurationPostProcessor這個類咱們就能夠幹掉了,由於它此時已經徹底沒什麼用了。咱們用和上面同樣的測試代碼,發現最後的輸出了咱們想要的東西,此時,完整的一個整合就結束了。

多說一下關於Import註解的東西,你只須要記住,當Import進來的類,沒有實現ImportBeanDefinitionRegistrar這個接口的時候,這個類就會被放進Spring容器中, 你能夠經過@Autowired的方式去自動注入它;反之若是實現了ImportBeanDefinitionRegistrar,那麼這個類以後是不會放入Spring中,這個緣由涉及到的代碼在ConfigurationClassPostProcessor的方法postProcessBeanDefinitionRegistry中,具體是仍是比較複雜的,我後邊若是有時間也會專門再去寫文章講這些。

關於FactoryBean的面試題

相信很多人在面試中是遇到過這樣一個面試題的:你能說說FactoryBean和BeanFactory的區別嗎?咱們能夠說他們都是工廠,都是用來建立對象的,可是建立對象的場景是天差地別的,BeanFactory是能夠用來建立各類各樣的對象,可是FactoryBean是用來建立某一類的複雜對象的。而且BeanFactory人家的實現類均可以說是一個一個的容器,可是FactoryBean就不是了。

相關文章
相關標籤/搜索