SpringBoot 應用篇之從 0 到 1 實現一個自定義 Bean 註冊器

191213-SpringBoot 應用篇之從 0 到 1 實現一個自定義 Bean 註冊器java


咱們知道在 spring 中能夠經過@Component@Service, @Repository 裝飾一個類,經過自動掃描註冊爲 bean;也能夠經過在配置類中,藉助@Bean來註冊 bean;那麼除了這幾種方式以外,還有什麼其餘的方式來聲明一個類爲 bean 麼?git

咱們是否能夠自定義一個註解,而後將這個註解裝飾的類主動聲明爲 bean 註冊到 spring 容器,從而實現相似@Component的效果呢?github

接下來本文將介紹,若是經過ImportBeanDefinitionRegistrar結合自定義註解來實現 bean 註冊,主要用到的知識點以下:web

  • ImportBeanDefinitionRegistrar bean 註冊的核心類
  • @Import 導入配置
  • ClassPathBeanDefinitionScanner

<!-- more -->spring

I. 自定義 bean 註冊器

雖然咱們的目標比較清晰,可是忽然讓咱們來實現這麼個東西,還真有點手足無措,應該從哪裏下手呢?ide

0. 尋找"致敬"對象

若是看過我以前關於 SpringBoot 結合 java web 三劍客(Filter, Servlet, Listener)的相關博文的同窗,應該會記得一個重要的知識點:spring-boot

  • @WebListener, @WebServlet, @WebFilter 這三個註解屬於 Servlet3+ 規範
  • 在 SpringBoot 項目中,如須要上面註解生效,須要在啓動類上添加註解 @ServletComponentScan

看到上面這個是否是會有一絲靈感被激發(在當時寫上面博文的時候,特地的看了一下後面註解的邏輯),嘿嘿,感受找到了一條通往成功之旅的道路學習

既然@WebXxx註解不是原生的 Spring 支持註解,因此讓他生效的註解 @ServletComponentScan就顯得很重要了,顯然是它充當了橋樑(在搞事情了),而後咱們致敬(抄襲)的對象就有了測試

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServletComponentScanRegistrar.class)
public @interface ServletComponentScan {
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};
}

註解定義比較簡單,最終生效的不用說,確定是ServletComponentScanRegistrar了,再接着瞅一眼ui

(不一樣的 SpringBoot 版本,上面的實現類可能會有必定的差別,上面的源碼截取自 spring-boot 2.1.2.RELEASE 版本的包內)

1. 準備篇

致敬對象找到了,接下來開始正式實現前的一些準備工做,首先咱們把目標具體事例化

  • 全部類上擁有自定義註解@Meta的類,會註冊到 Spring 容器,做爲一個普通的 Bean 對象

而後就是測試測試驗證是否生效的關鍵 case 了

  • 無外部依賴的@Meta類是否能夠正常被 spring 識別
  • @Meta類是否能夠被其餘bean or @Meta類經過@Autowired引入
  • @Meta類是否能夠正常依賴普通的bean@Meta

2. 開始實現

a. @Meta 註解定義

相似@Component註解的功能,咱們弄簡單一點便可

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}

b. @MetaComponentScan 註解

這個註解和@ServletComponentScan做用差很少,主要是用來加載ImportBeanDefinitionRegistrar實現類,後者則是定義 bean 的核心類

實現以下

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MetaAutoConfigureRegistrar.class})
public @interface MetaComponentScan {
    @AliasFor("basePackages") String[] value() default {};

    @AliasFor("value") String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

先暫時無視 Import 的值,看一下註解的basePackagesbasePackageClasses

咱們知道@ComponentScan的做用主要是用來指定哪些包路徑下的類開啓註解掃描;MetaComponentScan的幾個成員主要做用和上面相同;

  • 當指定了值的時候,主要加載這些包路徑下,包含@Meta註解的類;
  • 若是全是默認值(即爲空),則掃描這個註解所在類對應的包路徑下全部包含@Meta的類

c. MetaAutoConfigureRegistrar

接下來進入咱們的核心類,它主要繼承自ImportBeanDefinitionRegistrar,bean 定義註冊器,其核心方法爲

void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

兩個參數,第一個顧名思義,註解元數據,多半是用來獲取註解的屬性;第二個 bean 定義註冊器,咱們在學習 bean 的動態註冊時(詳情參考: - 181013-SpringBoot 基礎篇 Bean 之動態註冊) 知道能夠用 BeanDefinitionRegistry 註冊 bean,由於咱們這裏的目標是註冊全部帶 @Meta 註解的類

天然而然的想法

  • 掃描全部的類,判斷是否有@Meta註解,有則經過 registry 手動註冊

然而在實際動手以前,再稍微停一停;掃描全部類判斷是否有某個註解,這個操做在 spring 中應該屬於比較常見的 case(why?),應該是有一些可供咱們使用的輔助類

繼續擼"致敬"的對象,ServletComponentScanRegistrar類主要是註冊servletComponentRegisteringPostProcessor,因此咱們再轉移目標到後者的詳情(下圖來自org.springframework.boot.web.servlet.ServletComponentRegisteringPostProcessor#createComponentProvider)

到這裏咱們的思路又打開了,能夠藉助ClassPathScanningCandidateComponentProvider來實現 bean 註冊


上面的一段內容屬於前戲,放在腦海裏迅速的過一過就行了,接下來進入正文;

首先是建立一個ClassPathScanningCandidateComponentProvider的子類,註冊一個AnnotationTypeFilter,確保過濾獲取全部@Meta註解的類

private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
    public MetaBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
            Environment environment, ResourceLoader resourceLoader) {
        super(registry, useDefaultFilters, environment, resourceLoader);
        registerFilters();
    }

    protected void registerFilters() {
        addIncludeFilter(new AnnotationTypeFilter(Meta.class));
    }
}

而後就是獲取掃描的包路徑了,經過解析前面定義的MetaComponentScan的屬性來獲取

private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
    AnnotationAttributes attributes =
            AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(MetaComponentScan.class.getName()));
    String[] basePackages = attributes.getStringArray("basePackages");
    Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");

    Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
    for (Class clz : basePackageClasses) {
        packagesToScan.add(ClassUtils.getPackageName(clz));
    }

    if (packagesToScan.isEmpty()) {
        packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
    }

    return packagesToScan;
}

因此完整的 MetaAutoConfigureRegistrar 的實現就有了

public class MetaAutoConfigureRegistrar
        implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        MetaBeanDefinitionScanner scanner =
                new MetaBeanDefinitionScanner(registry, this.environment, this.resourceLoader);
        Set<String> packagesToScan = this.getPackagesToScan(importingClassMetadata);
        scanner.scan(packagesToScan.toArray(new String[]{}));
    }

    private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
      // ... 參考前面,這裏省略
    }

    private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
      // ... 參考前面,這省略
    }
}

II. 測試與小結

上面實現如今看來很是簡單了(兩個註解定義,一個核心類,也複雜不到哪裏去了);接下來就須要驗證這個是否生效了

1. case0 Meta 註解類

若是被 spring 識別爲 bean,則構造方法會被調用

@Meta
public class DemoBean1 {
    public  DemoBean1() {
        System.out.println("DemoBean1 register!");
    }
}

2. case1 Meat 註解類,依賴 Bean

定義一個普通的 bean 對象

@Component
public class NormalBean {
    public NormalBean() {
        System.out.println("normal bean");
    }
}

而後定義一個 Meta 裝飾的類,依賴 NormalBean

@Meta
public class DependBean {
    public DependBean(NormalBean normalBean) {
        System.out.println("depend bean! " + normalBean);
    }
}

3. case2 bean 依賴 Meta 註解類

@Component
public class ABean {
    public ABean(DemoBean1 demoBean1) {
        System.out.println("a bean : " + demoBean1);
    }
}

4. 測試

啓動類,注意須要添加上咱們自定義的@MetaComponentScan註解

@SpringBootApplication
@MetaComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

執行輸出結果

5. 小結

本文主要介紹瞭如何經過ImportBeanDefinitionRegistrar來實現自定義的 bean 註冊器的全過程,包括面向新手能夠怎樣經過"致敬"既有的代碼邏輯,來"巧妙"的實現咱們的目標

II. 其餘

0. 項目

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索