咱們知道在 spring 中能夠經過@Component
,@Service
, @Repository
裝飾一個類,經過自動掃描註冊爲 bean;也能夠經過在配置類中,藉助@Bean
來註冊 bean;那麼除了這幾種方式以外,還有什麼其餘的方式來聲明一個類爲 bean 麼?git
咱們是否能夠自定義一個註解,而後將這個註解裝飾的類主動聲明爲 bean 註冊到 spring 容器,從而實現相似@Component
的效果呢?github
接下來本文將介紹,若是經過ImportBeanDefinitionRegistrar
結合自定義註解來實現 bean 註冊,主要用到的知識點以下:web
ImportBeanDefinitionRegistrar
bean 註冊的核心類@Import
導入配置ClassPathBeanDefinitionScanner
<!-- more -->spring
雖然咱們的目標比較清晰,可是忽然讓咱們來實現這麼個東西,還真有點手足無措,應該從哪裏下手呢?ide
若是看過我以前關於 SpringBoot 結合 java web 三劍客(Filter, Servlet, Listener)的相關博文的同窗,應該會記得一個重要的知識點:spring-boot
@WebListener
, @WebServlet
, @WebFilter
這三個註解屬於 Servlet3+ 規範@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 版本的包內)
致敬對象找到了,接下來開始正式實現前的一些準備工做,首先咱們把目標具體事例化
@Meta
的類,會註冊到 Spring 容器,做爲一個普通的 Bean 對象而後就是測試測試驗證是否生效的關鍵 case 了
@Meta
類是否能夠正常被 spring 識別@Meta
類是否能夠被其餘bean
or @Meta
類經過@Autowired
引入@Meta
類是否能夠正常依賴普通的bean
,@Meta
類相似@Component
註解的功能,咱們弄簡單一點便可
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Meta { }
這個註解和@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 的值,看一下註解的basePackages
和basePackageClasses
咱們知道@ComponentScan
的做用主要是用來指定哪些包路徑下的類開啓註解掃描;MetaComponentScan
的幾個成員主要做用和上面相同;
@Meta
註解的類;@Meta
的類接下來進入咱們的核心類,它主要繼承自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) { // ... 參考前面,這省略 } }
上面實現如今看來很是簡單了(兩個註解定義,一個核心類,也複雜不到哪裏去了);接下來就須要驗證這個是否生效了
若是被 spring 識別爲 bean,則構造方法會被調用
@Meta public class DemoBean1 { public DemoBean1() { System.out.println("DemoBean1 register!"); } }
定義一個普通的 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); } }
@Component public class ABean { public ABean(DemoBean1 demoBean1) { System.out.println("a bean : " + demoBean1); } }
啓動類,注意須要添加上咱們自定義的@MetaComponentScan
註解
@SpringBootApplication @MetaComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
執行輸出結果
本文主要介紹瞭如何經過ImportBeanDefinitionRegistrar
來實現自定義的 bean 註冊器的全過程,包括面向新手能夠怎樣經過"致敬"既有的代碼邏輯,來"巧妙"的實現咱們的目標
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛