Spring Boot 自動裝配(一) - @Component、@ComponentScan、@Enable 模塊

前言

        最近在學習Spring Boot相關的課程,過程當中以筆記的形式記錄下來,方便之後回憶,同時也在這裏和你們探討探討,文章中有漏的或者有補充的、錯誤的都但願你們可以及時提出來,本人在此先謝謝了!web

開始以前呢,但願你們帶着幾個問題去學習:
一、Spring註解驅動是什麼?
二、這個功能在什麼時代背景下發明產生的?
三、這個功能有什麼用?
四、怎麼實現的?
五、優勢和缺點是什麼?
六、這個功能能應用在工做中?
這是對自個人提問,我認爲帶着問題去學習,是一種更好的學習方式,有利於加深理解。好了,接下來進入主題。spring

一、起源

        咱們先來簡單的聊聊Spring註解的發展史。Spring1.x時代,那時候註解的概念剛剛興起,僅支持如 @Transactional 等註解。到了2.x時代Spring的註解體系有了雛形,引入了 @Autowired 、 @Controller 這一系列骨架式的註解。3.x是黃金時代,它除了引入 @Enable 模塊驅動概念,加快了Spring註解體系的成型,還引入了配置類 @Configuration 及 @ComponentScan ,使咱們能夠拋棄XML配置文件的形式,全面擁抱Spring註解,但Spring並未徹底放棄XML配置文件,它提供了 @ImportResource 容許導入遺留的XML配置文件。此外還提供了 @Import 容許導入一個或多個Java類成爲Spring Bean。4.X則趨於完善,引入了條件化註解 @Conditional ,使裝配更加的靈活。當下是5.X時代,是SpringBoot2.0的底層核心框架,目前來看,變化不是很大,但也引入了一個 @Indexed 註解,主要是用來提高啓動性能的。好了,以上是Spring註解的發展史,接下來咱們對Spring註解體系的幾個議題進行講解。編程

二、Spring 模式註解

        模式註解是一種用於聲明在應用中扮演「組件」角色的註解。如 Spring 中的 @Repository 是用於扮演倉儲角色的模式註解,用來管理和存儲某種領域對象。還有如@Component 是通用組件模式、@Service 是服務模式、@Configuration 是配置模式等。其中@Component 做爲一種由 Spring 容器託管的通用模式組件,任何被 @Component 標註的組件均爲組件掃描的候選對象。相似地,凡是被 @Component 標註的註解,如@Service ,當任何組件標註它時,也被視做組件掃描的候選對象。
舉例:緩存

Spring註解 場景說明 起始版本
@Componnt 通用組件模式註解 2.5
@Repository 數據倉儲模式註解 2.0
@Service 服務模式註解 2.5
@Controller Web 控制器模式註解 2.5
@Configuration 配置類模式註解 3.0

那麼,被這些註解標註的類如何交由Spring來管理呢,或者說如何被Spring所裝配呢?接下來咱們就來看看Spring的兩種裝配方式。服務器

2.一、裝配方式

  • <context:component-scan> 方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd ">

    <context:component-scan base-package="com.loong.spring.boot" />
</beans>

第一種是XML配置文件的方式,經過 base-package 這個屬性指定掃描某個範圍內全部被 @Component 或者其派生註解標記的類(Class),將它們註冊爲 Spring Bean。app

咱們都知道XML Schema 規範,標籤須要顯示地關聯命名空間,如配置文件中的 xmlns:context="http://www.springframework.org/schema/context" ,且須要與其處理類創建映射關係,而該關係維護在相對於 classpath 下的/META-INF/spring.handlers 文件中。以下:框架

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

能夠看到, context 所對應的處理器爲 ContextNamespaceHandler異步

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        .....
        registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
        registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
        .....
    }
}

這裏當Spring啓動時,init方法被調用,隨後註冊該命名空間下的全部 Bean 定義解析器,能夠看到 的解析器爲 ComponentScanBeanDefinitionParser 。具體的處理過程就在此類中,感興趣的同窗能夠去深刻了解,這裏再也不贅述。ide

  • @ComponentScan 方式
@ComponentScan(basePackages = "com.loong.spring.boot")
public class SpringConfiguration {

}

第二種是註解的形式,一樣也是依靠 basePackages 屬性指定掃描範圍。

Spring 在啓動時,會在某個生命週期內建立全部的配置類註解解析器,而 @ComponentScan 的處理器爲 ComponentScanAnnotationParser ,感興趣的同窗能夠去深刻了解,這裏一樣再也不贅述。

2.二、派生性

咱們用自定義註解的方式來看一看文中提到的派生性:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repository
public @interface FirstLevelRepository {
    String value() default "";
}

能夠看到咱們自定義了一個
@FirstLevelRepository 註解,當前註解又標註了 @Repository,而 @Repository 又標註了 @Component 而且註解屬性一致(String value() default""),那麼就能夠表示當前註解包含了
@Repository 及 @Component 的功能。

派生性其實能夠分爲多層次的,如
@SprintBootApplication -> @SpringBootConfiguration -> @Configuration -> @Component 能夠看到@Component被派生了多個層次,但這種多層次的派生性Spring 4.0版本纔開始支持,Spring3.0僅支持兩層。

三、Spring @Enable 模塊驅動

        前文提到Spring3.X是一個黃金時代,它不只全面擁抱註解模式,還開始支持「@Enable模塊驅動」。所謂「模塊」是指具有相同領域的功能組件集合,組合所造成的一個獨立的單元,好比 Web MVC 模塊、AspectJ代理模塊、Caching(緩存)模塊、JMX(Java 管理擴展)模塊、Async(異步處理)模塊等。這種「模塊」理念在後續的Spring 、Spring Boot和Spring Cloud版本中都一直被使用,這種模塊化的註解均以 @Enable 做爲前綴,以下所示:

框架實現 @Enable註解模塊 激活模塊
Spring Framework @EnableWebMvc Web Mvc 模塊
/ @EnableTransactionManagement 事物管理模塊
/ @EnableWebFlux Web Flux 模塊
Spring Boot @EnableAutoConfiguration 自動裝配模塊
/ @EnableConfigurationProperties 配置屬性綁定模塊
/ @EnableOAuth2Sso OAuth2 單點登錄模塊
Spring Cloud @EnableEurekaServer Eureka 服務器模塊
/ @EnableFeignClients Feign 客戶端模塊
/ @EnableZuulProxy 服務網關 Zuul 模塊
/ @EnableCircuitBreaker 服務熔斷模塊

引入模塊驅動的意義在於簡化裝配步驟,屏蔽了模塊中組件集合裝配的細節。但該模式必須手動觸發,也就是將該註解標註在某個配置Bean中,同時理解原理和加載機制的成本較高。那麼,Spring是如何實現 @Enable 模塊呢?主要有如下兩種方式。

3.一、Spring框架中@Enable實現方式

  • 基於 @Import 註解
    首先,參考 @EnableWebMvc 的實現:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
    
}

@Configuration 
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    ... 
}

這種實現模式主要是經過 @Import 導入配置類 DelegatingWebMvcConfiguration ,而該類標註了 @Configuration 註解,代表這是個配置類,咱們都知道 @EnableWebMvc 是用來激活Web MVC模塊,因此如HandlerMapping 、HandlerAdapter這些和MVC相關的組件都是在這個配置類中被組裝,這也就是所謂的模塊理念。

  • 基於接口編程

基於接口編程一樣有兩種實現方式,第一種參考 @EnableCaching的實現:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class) 
public @interface EnableCaching {
    ...
}

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
    
    @Override
    public String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) { //switch語句選擇實現模式
            case PROXY:
                return new String[]{AutoProxyRegistrar.class.getName(), ProxyCachingConfiguration.class.getName()};
            case ASPECTJ:
                return new String[]{AnnotationConfigUtils.CACHE_ASPECT_CONFIGURATION_CLASS_NAME};
            default:
        }
    }
}

這種方式主要是繼承 ImportSelector 接口(AdviceModeImportSelector實現了ImportSelector接口),而後實現 selectImports 方法,經過入參進而動態的選擇一個或多個類進行導入,相較於註解驅動,此方法更具備彈性。

第二種參考 @EnableApolloConfig 的實現:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApolloConfigRegistrar.class)
public @interface EnableApolloConfig {
    ....
}
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ....

        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(),
        PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues);

        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(),
        PropertySourcesProcessor.class);

        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
        ApolloAnnotationProcessor.class);

        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class);
        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class.getName(), SpringValueDefinitionProcessor.class);

        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(),
            ApolloJsonValueProcessor.class);
    }
}

這種方式主要是經過 @Import 導入實現了 ImportBeanDefinitionRegistrar 接口的類,在該類中重寫 registerBeanDefinitions 方法,經過 BeanDefinitionRegistry 直接手動註冊和該模塊相關的組件。接下來,咱們用這兩種方式實現自定義的 @Enable 模塊。

3.二、自定義@Enable模塊實現

  • 基於 @Import 註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HelloWorldConfiguration.class)
public @interface EnableHelloWorld {
}
@Configuration
public class HelloWorldConfiguration {

    // 能夠作一些組件初始化的操做。

    @Bean
    public String helloWorld(){ 
        return "hello world";
    }
    // ....
}
@EnableHelloWorld
public class EnableHelloWorldBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloWorldBootstrap.class)
                .web(WebApplicationType.NONE).run(args);
        String helloWorld = context.getBean("helloWorld",String.class);
        System.out.println(helloWorld );
    }
}

這裏咱們自定義了一個 @EnableHelloWorld 註解,再用 @Import 導入一個自定義的配置類 HelloWorldConfiguration,在這個配置類中初始化 helloWorld 。

  • 基於接口編程
    第一種基於 ImportSelector 接口:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HelloWorldImportSelector.class)
public @interface EnableHelloWorld {
}
public class HelloWorldImportSelector implements ImportSelector {
    /**
     * 這種方法比較有彈性:
     *  能夠調用importingClassMetadata裏的方法來進行條件過濾
     *  具體哪些方法參考:https://blog.csdn.net/f641385712/article/details/88765470
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        if (importingClassMetadata.hasAnnotation("com.loong.case3.spring.annotation.EnableHelloWorld")) {
           return new String[]{HelloWorldConfiguration.class.getName()};
        }
    }
}
@Configuration
public class HelloWorldConfiguration {
    // 能夠作一些組件初始化的操做

    @Bean
    public String helloWorld(){ 
        return "hello world";
    }
    // ....
}
@EnableHelloWorld
public class EnableHelloWorldBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloWorldBootstrap.class)
                .web(WebApplicationType.NONE).run(args);
        String helloWorld = context.getBean("helloWorld",String.class);
        System.out.println(helloWorld );
    }
}

這裏咱們一樣是自定義 @EnableHelloWorld 註解,經過 @Import 導入 HelloWorldImportSelector 類,該類實現了 ImportSelector 接口,在重寫的方法中經過 importingClassMetadata.hasAnnotation("com.loong.case3.spring.annotation.EnableHelloWorld") 判斷該類是否標註了 @EnableHelloWorld 註解,從而導入 HelloWorldConfiguration 類,進行初始化工做。

第二種基於 ImportBeanDefinitionRegistrar 接口:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HelloWorldRegistrar.class)
public @interface EnableHelloWorld {
}
public class HelloWorldRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        if (annotationMetadata.hasAnnotation("com.loong..case4.spring.annotation.EnableHelloWorld")) {
            RootBeanDefinition beanDefinition = new RootBeanDefinition(HelloWorldConfiguration.class);
            beanDefinitionRegistry.registerBeanDefinition(HelloWorldConfiguration.class.getName(), beanDefinition);
        }
    }
}
@Configuration
public class HelloWorldConfiguration {
    public HelloWorldConfiguration() {
        System.out.println("HelloWorldConfiguration初始化....");
    }
}
@EnableHelloWorld
public class EnableHelloWorldBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloWorldBootstrap.class)
                .web(WebApplicationType.NONE).run(args);
    }
}

這裏就是在 HelloWorldRegistrar 中利用 BeanDefinitionRegistry 直接註冊 HelloWorldConfiguration。

四、Spring 條件裝配

        條件裝配指的是經過一些列操做判斷是否裝配 Bean ,也就是 Bean 裝配的前置判斷。實現方式主要有兩種:@Profile 和 @Conditional,這裏咱們主要講 @Conditional 的實現方式,由於 @Profile 在 Spring 4.0 後也是經過 @Conditional 來實現。

@Conditional(HelloWorldCondition.class)
@Component
public class HelloWorldConfiguration {
    public HelloWorldConditionConfiguration (){
        System.out.println("HelloWorldConfiguration初始化。。。");
    }
}
public class HelloWorldCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        // ...
        return true;
    }
}

這裏經過自定義一個 HelloWorldConfiguration 配置類,再標註 @Conditional 註解導入 HelloWorldCondition類,該類必須實現 Condition 接口,而後重寫 matches 方法,在方法中能夠經過兩個入參來獲取一系列的上下文數據和元數據,最終返回ture或false來斷定該類是否初始化,

五、總結

        關於Spring註解驅動的概念就告一段落,最後來簡單的回顧下這篇文章的內容,這篇文章主要講了 Spring 註解相關的幾個概念:Spring模式註解、@Enable 模塊驅動和 Spring 的條件裝配。其中 Spring 模式註解的核心是 @Component,全部的模式註解均被它標註,而對應兩種裝配方式實際上是尋找 @Component 的過程。Spring @Enable 模塊的核心是在 @Enable 註解上經過 @Import 導入配置類 ,從而在該配置類中實現和當前模塊相關的組件初始化工做。能夠看到,Spring 組件裝配並不具有自動化,都須要手動標註多種註解,且之間需相互配合,因此下一章咱們就來說講 Spring Boot是如何基於 Spring 註解驅動來實現自動裝配的。

以上就是本章的內容,如過文章中有錯誤或者須要補充的請及時提出,本人感激涕零。



參考:

《Spring Boot 編程思想》

相關文章
相關標籤/搜索