你真的知道Spring註解驅動的前世今生嗎?這篇文章讓你豁然開朗!

跟著Mic學架構發表於2021-11-25

圖怪獸_40b0b39fdbb3d29e40b4b52282058329_23747

本篇文章,從Spring1.x到Spring 5.x的迭代中,站在現在的角度去思考Spring註解驅動的發展過程,這將有助於我們更好的理解Spring中的註解設計。

Spring Framework 1.x

在SpringFramework1.x時代,其中在1.2.0是這個時代的分水嶺,當時Java5剛剛釋出,業界正興起了使用Annotation的技術風,Spring Framework自然也提供了支援,比如當時已經支援了@Transactional等註解,但是這個時候,XML配置方式還是唯一選擇。

  • 在xml中新增Bean的宣告

    <bean name="testService" class="com.gupaoedu.controller.TestService"/>
    
  • 測試

    public class XmlMain {
        public static void main(String[] args) {
            ApplicationContext context=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
            TestService testService=(TestService)context.getBean("testService");
            System.out.println(testService);
        }
    }
    

Spring Framework 2.x

Spring Framework2.x時代,2.0版本在Annotation中新增了@Required、@Repository以及AOP相關的@Aspect等註解,同時也提升了XML配置能力,也就是可擴充套件的XML,比如Dubbo這樣的開源框架就是基於Spring XML的擴充套件來完美的整合Spring,從而降低了Dubbo使用的門檻。

在2.x時代,2.5版本也是這個時代的分水嶺, 它引入了一些很核心的Annotation

  • Autowired 依賴注入
  • @Qualifier 依賴查詢
  • @Component、@Service 元件宣告
  • @Controller、@RequestMappring等spring mvc的註解

儘管Spring 2.x時代提供了不少的註解,但是仍然沒有脫離XML配置驅動,比如context:annotation-config context:componet-scan , 前者的職責是註冊Annotation處理器,後者是負責掃描classpath下指定包路徑下被Spring模式註解標註的類,將他們註冊成為Spring Bean

  • 在applicationContext.xml中定義context:componet-scan

    <context:component-scan base-package="com.gupaoedu.controller"/>
    
  • 新增註解宣告

    @Service
    public class TestService {
    }
    
  • 測試類

    public class XmlMain {
        public static void main(String[] args) {
            ApplicationContext context=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
            TestService testService=(TestService)context.getBean("testService");
            System.out.println(testService);
        }
    }
    

Spring Framework 3.x

Spring Framework3.0是一個里程碑式的時代,他的功能特性開始出現了非常大的擴充套件,比如全面擁抱Java5、以及Spring Annotation。更重要的是,它提供了配置類註解@Configuration, 他出現的首要任務就是取代XML配置方式,不過比較遺憾的是,Spring Framework3.0還沒有引入替換XML元素context:componet-scan的註解,而是選擇了一個過渡方式@ImportResource。

@ImportResource允許匯入遺留的XML配置檔案,比如

@ImportResource("classpath:/META-INF/spring/other.xml")
@Configuration
public class SpringConfiguration{
    
}

並且在Spring Frameworkd提供了AnnotationConfigApplicationContext註冊,用來註冊@Configuration Class,通過解析Configuration類來進行裝配。

在3.1版本中,引入了@ComponentScan,替換了XML元素Context:component-scan , 這個註解雖然是一個小的升級,但是對於spring 來說在註解驅動領域卻是一個很大的進步,至此也體現了Spring 的無配置化支援。

Configuration配置演示

  • Configuration這個註解大家應該有用過,它是JavaConfig形式的基於Spring IOC容器的配置類使用的一種註解。因為SpringBoot本質上就是一個spring應用,所以通過這個註解來載入IOC容器的配置是很正常的。所以在啟動類裡面標註了@Configuration,意味著它其實也是一個IoC容器的配置類。

    舉個非常簡單的例子

  • 測試程式碼

    ConfigurationDemo
    @Configuration
    public class ConfigurationDemo {
        @Bean
        public DemoClass demoClass(){
            return new DemoClass();
        }
    }
    DemoClass
    public class DemoClass {
    
        public void say(){
            System.out.println("say: Hello Mic");
        }
    }
    ConfigurationMain
    public class ConfigurationMain {
    
        public static void main(String[] args) {
            ApplicationContext applicationContext=
                    new AnnotationConfigApplicationContext
                            (ConfigurationDemo.class);
            DemoClass demoClass=applicationContext.getBean(DemoClass.class);
            demoClass.say();
        }
    }
    
    

Component-scan

ComponentScan這個註解是大家接觸得最多的了,相當於xml配置檔案中的context:component-scan。 它的主要作用就是掃描指定路徑下的標識了需要裝配的類,自動裝配到spring的Ioc容器中。

標識需要裝配的類的形式主要是:@Component、@Repository、@Service、@Controller這類的註解標識的類。

  • 在spring-mvc這個工程中,建立一個單獨的包路徑,並建立一個OtherServcie。

    @Service
    public class OtherService {
    }
    
  • 在Controller中,注入OtherService的例項,這個時候訪問這個介面,會報錯,提示沒有otherService這個例項。

    @RestController
    public class HelloController {
    
        @Autowired
        OtherService otherService;
    
        @GetMapping("/hello")
        public String hello(){
            System.out.println(otherService);
            return "Hello Gupaoedu";
        }
    }
    
  • 新增conpoment-scan註解,再次訪問,錯誤解決。

    @ComponentScan("com.gupaoedu")
    

ComponentScan預設會掃描當前package下的的所有加了相關注解標識的類到IoC容器中;

Import註解

import註解是什麼意思呢? 聯想到xml形式下有一個<import resource/> 形式的註解,就明白它的作用了。import就是把多個分來的容器配置合併在一個配置中。在JavaConfig中所表達的意義是一樣的。

  • 建立一個包,並在裡面新增一個單獨的configuration

    public class DefaultBean {
    }
    @Configuration
    public class SpringConfig {
    
        @Bean
        public DefaultBean defaultBean(){
            return new DefaultBean();
        }
    }
    
  • 此時執行測試方法,

    public class MainDemo {
    
        public static void main(String[] args) {
            ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
            String[] defNames=ac.getBeanDefinitionNames();
            for(String name:defNames){
                System.out.println(name);
            }
        }
    }
    
  • 在另外一個包路徑下在建立一個配置類。此時再次執行前面的測試方法,列印OtherBean例項時,這個時候會報錯,提示沒有該例項

    public class OtherBean {
    }
    @Configuration
    public class OtherConfig {
    
        @Bean
        public OtherBean otherBean(){
            return new OtherBean();
        }
    }
    
  • 修改springConfig,把另外一個配置匯入過來

    @Import(OtherConfig.class)
    @Configuration
    public class SpringConfig {
    
        @Bean
        public DefaultBean defaultBean(){
            return new DefaultBean();
        }
    }
    
  • 再次執行測試方法,即可看到物件例項的輸出。

至此,我們已經瞭解了Spring Framework在註解驅動時代,完全替代XML的解決方案。至此,Spring團隊就此止步了嗎?你們太單純了。雖然無配置化能夠減少配置的維護帶來的困擾,但是,還是會存在很對第三方組建的基礎配置宣告。同樣很繁瑣,所以Spring 退出了@Enable模組驅動。這個特性的作用是把相同職責的功能元件以模組化的方式來裝配,更進一步簡化了Spring Bean的配置。

Enable模組驅動

我們通過spring提供的定時任務機制來實現一個定時任務的功能,分別拿演示在使用Enable註解和沒使用Enable的區別。讓大家感受一些Enable註解的作用。

使用EnableScheduing之前

  • 在applicationContext.xml中新增定時排程的配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:task="http://www.springframework.org/schema/task"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task-3.2.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="com.gupaoedu.controller"/>
        <!--AnnotationDrivenBeanDefinitionParser-->
        <task:annotation-driven scheduler="scheduler"/> <!-- 定時器開關-->
        <task:scheduler id="scheduler" pool-size="5"/>
    </beans>
    
  • 編寫任務處理類

    @Service
    public class TaskService {
    
        @Scheduled(fixedRate = 5000) //通過@Scheduled宣告該方法是計劃任務,使用fixedRate屬性每隔固定時間執行
        public void reportCurrentTime(){
            System.out.println("每隔5秒執行一次 "+new Date());
        }
    }
    
  • 編寫測試類

    public class TestTask {
    
        public static void main(String[] args) {
            ApplicationContext applicationContext=new FileSystemXmlApplicationContext("classpath:applicationContext.xml");
    
        }
    }
    

使用EnableScheding之後

  • 建立一個配置類

    @Configuration
    @ComponentScan("com.gupaoedu.controller")
    @EnableScheduling
    public class SpringConfig {
    }
    
  • 建立一個service

    @Service
    public class TaskService {
        @Scheduled(fixedRate = 5000) //通過@Scheduled宣告該方法是計劃任務,使用fixedRate屬性每隔固定時間執行
        public void reportCurrentTime(){
            System.out.println("每隔5秒執行一次 "+new Date());
        }
    }
    
  • 建立一個main方法

    public class TaskMain {
        public static void main(String[] args) {
            ApplicationContext context=new AnnotationConfigApplicationContext(SpringConfig.class);
        }
    }
    
  • 啟動服務即可實現定時排程的功能。

思考使用Enable省略了哪個步驟呢?

首先我們看沒使用Enable的程式碼,它裡面會有一個

<task:annotation-driven scheduler="scheduler"/>

這個scheduler是一個註解驅動,會被AnnotationDrivenBeanDefinitionParser 這個解析器進行解析。

在parse方法中,會有如下程式碼的定義

 builder = BeanDefinitionBuilder.genericBeanDefinition("org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor");
            builder.getRawBeanDefinition().setSource(source);

這個類是用來解析@Scheduled註解的。

ok,我們再看一下EnableScheduling註解,我們可以看到,它會自動註冊一個ScheduledAnnotationBeanPostProcessor的bean。所以,通過這個例子,就是想表達Enable註解的作用,它可以幫我們省略一些第三方模組的bean的宣告的配置。

public class SchedulingConfiguration {
    public SchedulingConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
    )
    @Role(2)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

Spring Framework 4.x

Spring 4.x版本,是註解的完善時代,它主要是提升條件裝配能力,引入了@Conditional註解,通過自定義Condition實現配合,彌補了之前版本條件化配置的短板。

簡單來說,Conditional提供了一個Bean的裝載條件判斷,也就是說如果這個條件不滿足,那麼通過@Bean宣告的物件,不會被自動裝載進來,具體是怎麼用的呢?,先來簡單帶大家瞭解一下它的基本使用。

Conditional的概述

@Conditional是一個註解,我們觀察一下這個註解的宣告, 它可以接收一個Condition的陣列。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

這個Condition是一個函式式介面,提供了一個matchers的方法,簡單來說,它就是提供了一個匹配的判斷規則,返回true表示可以注入bean,返回false表示不能注入。

Conditional的實戰

  • 自定義個一個Condition,邏輯比較簡單,如果當前作業系統是Windows,則返回true,否則返回false

    public class GpCondition implements Condition{
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata
        annotatedTypeMetadata) {
            //此處進行條件判斷,如果返回 true,表示需要載入該配置類或者 Bean
            //否則,表示不載入
            String os=conditionContext.getEnvironment().getProperty("os.name");
            if(os.contains("Windows")){
           	 	return true;
            }
            	return false;
        }
    }
    
  • 建立一個配置類,裝載一個 BeanClass

    @Configuration
    public class ConditionConfig {
        @Bean
        @Conditional(GpCondition.class)
        public BeanClass beanClass(){
       	 	return new BeanClass();
        }
    }
    
  • 在 BeanClass 的 bean 宣告方法中增加@Conditional(GpCondition.class),其中具體的條件是我們自定義的 GpCondition 類。上述程式碼所表達的意思是,如果 GpCondition 類中的 matchs 返回 true,則將 BeanClass 裝載到 Spring IoC 容器中

  • 執行測試方法

    public class ConditionMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context=new
        AnnotationConfigApplicationContext(ConditionConfig.class);
        BeanClass beanClass=context.getBean(BeanClass.class);
        System.out.println(beanClass);
        }
    }
    

總結

經過對Spring註解驅動的整體分析,不難發現,我們如今之所以能夠非常方便的基於註解來完成Spring中大量的功能,得益於Spring團隊不斷解決使用者痛點而做的各種努力。
而Spring Boot的自動裝配機制,也是在Spring 註解驅動的基礎上演化而來,在後續的內容中,我會專門分析Spring Boot的自動裝配機制。

版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術乾貨!

相關文章