@Import :Spring Bean模組裝配的藝術

2023-07-18 18:00:43

本文分享自華為雲社群《Spring高手之路8——Spring Bean模組裝配的藝術:@Import詳解》,作者: 磚業洋__。

本文將帶你深入探索Spring框架的裝配機制,以及它如何使你的程式碼更具模組化和靈活性。我們首先介紹Spring手動裝配的基礎知識,然後進一步解析@Import註解在模組裝配中的關鍵角色。文章涵蓋從匯入普通類、設定類,到使用ImportSelector和ImportBeanDefinitionRegistrar進行動態和選擇性裝配等多個層次,旨在幫助讀者全面理解和掌握Spring的裝配技術。

1. Spring手動裝配基礎

Spring中,手動裝配通常是指通過XML組態檔明確指定Bean及其依賴,或者在程式碼中直接使用new關鍵字建立物件並設定依賴關係。

然而,隨著Spring 2.0引入註解,以及Spring 3.0全面支援註解驅動開發,這個過程變得更加自動化。例如,通過使用@Component + @ComponentScanSpring可以自動地找到並建立bean,通過@AutowiredSpring可以自動地注入依賴。這種方式被稱為 「自動裝配」。

對於手動裝配,最常見的場景可能是在不使用Spring的上下文的單元測試或者簡單的POJO類中,通過new關鍵字直接建立物件和設定依賴關係。比如下面這段程式碼:

public class Main {
    public static void main(String[] args) {
        ServiceA serviceA = new ServiceA();
        ServiceB serviceB = new ServiceB(serviceA);
        //...
    }
}

在這個例子中,我們顯式地建立了ServiceAServiceB的物件,並將ServiceA的物件作為依賴傳遞給了ServiceB。這就是一個典型的手動裝配的例子。

需要注意的是,手動裝配的使用通常是有限的,因為它需要開發者顯式地在程式碼中管理物件的建立和依賴關係,這在大型應用中可能會變得非常複雜和難以管理。因此,Spring的自動裝配機制(例如@Autowired註解,或者@Configuration@Bean的使用)通常是更常見和推薦的方式。

2. Spring框架中的模組裝配

模組裝配就是將我們的類或者元件註冊到SpringIoCInversion of Control,控制反轉)容器中,以便於Spring能夠管理這些類,並且在需要的時候能夠為我們自動地將它們注入到其他的元件中。

Spring框架中,有多種方式可以實現模組裝配,包括:

  1. 基於Java的設定:通過使用@Configuration@Bean註解在Java程式碼中定義的Bean。這是一種宣告式的方式,我們可以明確地控制Bean的建立過程,也可以使用@Value@PropertySource等註解來處理設定屬性。

  2. 基於XML的設定Spring也支援通過XML組態檔定義Bean,這種方式在早期的Spring版本中更常見,但現在基於Java的設定方式更為主流。

  3. 基於註解的元件掃描:通過使用@Component@Service@Repository@Controller等註解以及@ComponentScan來自動檢測和註冊Bean。這是一種隱式的方式,Spring會自動掃描指定的包來查詢帶有這些註解的類,並將這些類註冊為Bean

  4. 使用@Import:這是一種顯式的方式,可以通過它直接註冊類到IOC容器中,無需這些類帶有@Component或其他特殊註解。我們可以使用它來註冊普通的類,或者註冊實現了ImportSelectorImportBeanDefinitionRegistrar介面的類,以提供更高階的裝配能力。

每種方式都有其應用場景,根據具體的需求,我們可以選擇合適的方式來實現模組裝配。比如在Spring Boot中,我們日常開發可能會更多地使用基於Java的設定和基於註解的元件掃描來實現模組裝配。

2.1 @Import註解簡單使用

@Import是一個強大的註解,它為我們提供了一個快速、方便的方式,使我們可以將需要的類或者設定類直接裝配到Spring IOC容器中。這個註解在模組裝配的上下文中特別有用。

我們先來看一下簡單的應用,後面再詳細介紹

全部程式碼如下:

Book.java

package com.example.demo.bean;

public class Book {
    private String name;

    public Book() {
        this.name = "Imported Book";
    }
    
    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                '}';
    }
}

LibraryConfig.java

package com.example.demo.configuration;

import com.example.demo.bean.Book;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(Book.class)
public class LibraryConfig {
}

使用 @Import 註解來匯入一個普通的類(即一個沒有使用 @Component 或者 @Service 之類的註解標記的類),Spring 會為該類建立一個 Bean,並且這個 Bean 的名字預設就是這個類的全限定類名。

主程式:

package com.example.demo;

import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        System.out.println(book);
    }
}

執行結果如下:

image.png

3. @Import模組裝配的四種方式

3.1 @Import註解的功能介紹

Spring中,有時候我們需要將某個類(可能是一個普通類,可能是一個設定類等等)匯入到我們的應用程式中。Spring提供了四種主要的方式來完成這個任務,後面我們會分別解釋。

@Import註解可以有以下幾種使用方式:

  • 匯入普通類:可以將普通類(沒有被@Component或者@Service等註解標註的類)匯入到SpringIOC容器中,Spring會為這個類建立一個Bean,這個Bean的名字預設為類的全限定類名。

  • 匯入設定類:可以將一個或多個設定類(被@Configuration註解標註的類)匯入到SpringIOC容器中,這樣我們就可以一次性地將這個設定類中定義的所有Bean匯入到SpringIOC容器中。

  • 使用ImportSelector介面:如果想動態地匯入一些BeanSpringIOC容器中,那麼可以實現ImportSelector介面,然後在@Import註解中引入ImportSelector實現類,這樣Spring就會將ImportSelector實現類返回的類匯入到SpringIOC容器中。

  • 使用ImportBeanDefinitionRegistrar介面:如果想在執行時動態地註冊一些BeanSpringIOC容器中,那麼可以實現ImportBeanDefinitionRegistrar介面,然後在@Import註解中引入ImportBeanDefinitionRegistrar實現類,這樣Spring就會將ImportBeanDefinitionRegistrar實現類註冊的Bean匯入到SpringIOC容器中。

@Import註解主要用於手動裝配,它可以讓我們顯式地匯入特定的類或者其他設定類到SpringIOC容器中。特別是當我們需要引入第三方庫中的類,或者我們想要顯式地控制哪些類被裝配進SpringIOC容器時,@Import註解會非常有用。它不僅可以直接匯入普通的 Java 類並將其註冊為 Bean,還可以匯入實現了 ImportSelector 或 ImportBeanDefinitionRegistrar 介面的類。這兩個介面提供了更多的靈活性和控制力,使得我們可以在執行時動態地註冊 Bean,這是通過 @Configuration + @Bean 註解組合無法做到的。

例如,通過 ImportSelector 介面,可以在執行時決定需要匯入哪些類。而通過 ImportBeanDefinitionRegistrar 介面,可以在執行時控制 Bean 的定義,包括 Bean 的名稱、作用域、構造引數等等。

雖然 @Configuration + @Bean 在許多情況下都足夠使用,但 @Import 註解由於其更大的靈活性和控制力,在處理更復雜的場景時,可能會是一個更好的選擇。

3.2 匯入普通類與自定義註解的使用

我們第2節的例子也是匯入普通類,這裡加一點難度,延伸到自定義註解的使用。

背景:圖書館模組裝配
在這個例子中,我們將建立一個圖書館系統,包括圖書館(Library)類、圖書館管理員(Librarian)類、圖書(Book)類,還有書架(BookShelf)類。我們的目標是建立一個圖書館,並將所有元件裝配到一起。

首先,我們建立一個自定義@ImportLibrary註解,通過此註解我們將把所有相關的類裝配到圖書館裡面:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({Librarian.class, Book.class, BookShelf.class})
public @interface ImportLibrary {
}

這個@ImportLibrary註解內部實際上使用了@Import註解。當Spring處理@Import註解時,會將其引數指定的類新增到Spring應用上下文中。當我們在Library類上使用@ImportLibrary註解時,Spring會將Librarian.classBook.classBookShelf.class這三個類新增到應用上下文中。

然後,我們建立圖書館管理員(Librarian)、圖書(Book)、書架(BookShelf)這三個類:

Librarian.java

package com.example.demo.bean;

public class Librarian {
    public void manage() {
        System.out.println("The librarian is managing the library.");
    }
}

Book.java

package com.example.demo.bean;

public class Book {
    private String name;

    // @ImportLibrary裡面有@Import會自動裝配,會呼叫無參構造,不寫會報錯
    public Book() {
    }

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

BookShelf.java

package com.example.demo.bean;

import java.util.List;

public class BookShelf {
    private List<Book> books;

    // @ImportLibrary裡面有@Import會自動裝配,會呼叫無參構造,不寫會報錯
    public BookShelf() {
    }

    public BookShelf(List<Book> books) {
        this.books = books;
    }

    public List<Book> getBooks() {
        return books;
    }
}

最後,我們建立一個圖書館(Library)類,並在這個類上使用我們剛剛建立的@ImportLibrary註解:

package com.example.demo.configuration;

import com.example.demo.annotations.ImportLibrary;
import com.example.demo.bean.Book;
import com.example.demo.bean.BookShelf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

@ImportLibrary
@Configuration
public class Library {

    @Bean
    public Book book1() {
        return new Book("The Catcher in the Rye");
    }

    @Bean
    public Book book2() {
        return new Book("To Kill a Mockingbird");
    }

    @Bean
    public BookShelf bookShelf(Book book1, Book book2) {
        return new BookShelf(Arrays.asList(book1, book2));
    }
}

然後我們可以建立一個啟動類並初始化IOC容器,看看是否可以成功獲取到Librarian類、BookShelf類和Book類的範例:

package com.example.demo;

import com.example.demo.bean.Book;
import com.example.demo.bean.BookShelf;
import com.example.demo.bean.Librarian;
import com.example.demo.configuration.Library;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Library.class);

        // 這行程式碼供偵錯檢視使用
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        
        Librarian librarian = context.getBean(Librarian.class);
        BookShelf bookShelf = context.getBean("bookShelf", BookShelf.class);
        Book book1 = (Book) context.getBean("book1");
        Book book2 = (Book) context.getBean("book2");

        librarian.manage();
        bookShelf.getBooks().forEach(book -> System.out.println("Book: " + book.getName()));
    }
}

這個例子中,我們通過@Import註解一次性把LibrarianBookBookShelf這三個類匯入到了SpringIOC容器中,這就是模組裝配的強大之處。

偵錯結果
image.png

當我們使用 @Import 註解來匯入一個普通的類(即一個沒有使用 @Component 或者 @Service 之類的註解標記的類),Spring 會為該類建立一個 Bean,並且這個 Bean 的名字預設就是這個類的全限定類名。

執行結果:

image.png

3.3 匯入設定類的策略

這裡使用Spring的 @Import註解匯入設定類,我們將建立一個BookConfig類和LibraryConfig類,然後在主應用類中獲取Book範例。

全部程式碼如下:

建立一個設定類BookConfig,用於建立和設定Book範例:

package com.example.demo.configuration;

import com.example.demo.bean.Book;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BookConfig {
    @Bean
    public Book book() {
        Book book = new Book();
        book.setName("Imported Book");
        return book;
    }
}

在這裡,我們定義了一個Book類:

package com.example.demo.bean;

public class Book {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

建立一個設定類LibraryConfig,使用@Import註解來匯入BookConfig類:

package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(BookConfig.class)
public class LibraryConfig {
}

主程式:

package com.example.demo;

import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        System.out.println(book.getName());
    }
}

執行結果:

image.png

在這個例子中,當Spring容器啟動時,它會通過@Import註解將BookConfig類匯入到Spring 上下文中,並建立一個Bean。然後我們可以在主程式中通過context.getBean(Book.class)獲取到Book的範例,並列印出書名。

3.4 使用ImportSelector進行選擇性裝配

如果我們想動態地選擇要匯入的類,我們可以使用一個ImportSelector實現。

全部程式碼如下:

定義一個 Book 類:

package com.example.demo.bean;

public class Book {
    private String name = "java從入門到精通";

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

建立圖書館管理員Librarian

package com.example.demo.bean;

public class Librarian {
    public void manage() {
        System.out.println("The librarian is managing the library.");
    }
}

定義一個 BookImportSelector,實現 ImportSelector 介面:

package com.example.demo.configuration;

import com.example.demo.bean.Librarian;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class BookImportSelector implements ImportSelector {

    /**
     * 這裡示範2種方式,一種是拿到class檔案後getName,一種是直接寫全限定類名
     * @param importingClassMetadata
     * @return
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] { "com.example.demo.bean.Book", Librarian.class.getName() };
    }
}

ImportSelector介面可以在執行時動態地選擇需要匯入的類。實現該介面的類需要實現selectImports方法,這個方法返回一個字串陣列,陣列中的每個字串代表需要匯入的類的全類名,我們可以直接在這裡將 Book 類和 Librarian 類加入到了 Spring 容器中。

使用Class.getName()方法獲取全限定類名的方式,比直接寫死類的全名為字串更推薦,原因如下:

  1. 避免錯誤:如果類名或包名有所改動,寫死的字串可能不會跟隨變動,這可能導致錯誤。而使用Class.getName()方法,則會隨類的改動自動更新,避免此類錯誤。
  2. 程式碼清晰:使用Class.getName()能讓讀程式碼的人更清楚地知道你是要參照哪一個類。
  3. 增強程式碼的可讀性和可維護性:使用類的位元組碼獲取全限定類名,使得程式碼閱讀者可以清晰地知道這是什麼類,增加了程式碼的可讀性。同時,也方便了程式碼的維護,因為在修改類名或者包名時,不需要手動去修改寫死的類名。

定義一個設定類 LibraryConfig,使用 @Import 註解匯入 BookImportSelector

package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(BookImportSelector.class)
public class LibraryConfig {
}

建立一個主應用類,從 Spring 的AnnotationConfigApplicationContext 中獲取 Book 的範例:

package com.example.demo;

import com.example.demo.bean.Book;
import com.example.demo.bean.Librarian;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        Librarian librarian = context.getBean(Librarian.class);
        System.out.println(book.getName());
        librarian.manage();
    }
}

執行結果:

image.png

在 Spring Boot 中,ImportSelector 被大量使用,尤其在自動設定(auto-configuration)機制中起著關鍵作用。例如,AutoConfigurationImportSelector 類就是間接實現了 ImportSelector,用於自動匯入所有 Spring Boot 的自動設定類。

我們通常會在Spring Boot啟動類上使用 @SpringBootApplication 註解,實際上,@SpringBootApplication 註解中也包含了 @EnableAutoConfiguration@EnableAutoConfiguration 是一個複合註解,它的實現中匯入了普通類 @Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector 類間接實現了 ImportSelector介面,用於自動匯入所有 Spring Boot 的自動設定類。

如下圖:

image.png

3.5 使用ImportBeanDefinitionRegistrar進行動態裝配

ImportBeanDefinitionRegistrar介面的主要功能是在執行時動態的往Spring容器中註冊Bean,實現該介面的類需要重寫registerBeanDefinitions方法,這個方法可以通過引數中的BeanDefinitionRegistry介面向Spring容器註冊新的類,給應用提供了更大的靈活性。

全部程式碼如下:

首先,定義一個 Book 類:

package com.example.demo.bean;

public class Book {
    private String name = "java從入門到精通";

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

定義一個 BookRegistrar 類,實現 ImportBeanDefinitionRegistrar 介面:

package com.example.demo.configuration;

import com.example.demo.bean.Book;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class BookRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Book.class);
        // 通過反射技術呼叫setter方法給name賦值,也可以在構造器賦值name,name需要呼叫beanDefinitionBuilder.addConstructorArgValue("戰爭與和平");
        beanDefinitionBuilder.addPropertyValue("name", "戰爭與和平");
        BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("myBook", beanDefinition);
    }
}

下面來詳細解釋一下BookRegistrar類裡面的registerBeanDefinitions方法和引數。

  • AnnotationMetadata importingClassMetadata: 這個參數列示當前被@Import註解匯入的類的所有註解資訊,它包含了該類上所有註解的詳細資訊,比如註解的名稱,註解的引數等等。

  • BeanDefinitionRegistry registry: 這個引數是SpringBean定義註冊類,我們可以通過它往Spring容器中註冊Bean。在這裡,我們使用它來註冊我們的Book Bean

在方法registerBeanDefinitions中,我們建立了一個BeanDefinition,並將其註冊到SpringBeanDefinitionRegistry中。

程式碼首先通過BeanDefinitionBuilder.genericBeanDefinition(Book.class)建立一個BeanDefinitionBuilder範例,這個範例用於構建一個BeanDefinition。我們使用addPropertyValue("name", "戰爭與和平")為該BeanDefinition新增一個name屬性值。

接著我們通過beanDefinitionBuilder.getBeanDefinition()方法得到BeanDefinition範例,並設定其作用域為原型作用域,這表示每次從Spring容器中獲取該Bean時,都會建立一個新的範例。

最後,我們將這個BeanDefinition以名字 "myBook" 註冊到BeanDefinitionRegistry中。這樣,我們就可以在Spring容器中通過名字 "myBook" 來獲取我們的Book類的範例了。

接著定義一個設定類 LibraryConfig,使用 @Import 註解匯入 BookRegistrar

package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(BookRegistrar.class)
public class LibraryConfig {
}

建立一個主應用類,從 Spring ApplicationContext 中獲取 Book 的範例:

package com.example.demo;

import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean("myBook", Book.class);
        System.out.println(book.getName());
    }
}

執行結果:

image.png

在這個例子中,我們使用 AnnotationConfigApplicationContext 初始化 Spring 容器並提供設定類。然後通過 context.getBean("book", Book.class) 從 Spring 容器中獲取名為 book 的範例。

ImportBeanDefinitionRegistrar介面提供了非常大的靈活性,我們可以根據自己的需求編寫任何需要的註冊邏輯。這對於構建複雜的、高度客製化的Spring應用是非常有用的。

Spring Boot就廣泛地使用了ImportBeanDefinitionRegistrar。例如,它的@EnableConfigurationProperties註解就是通過使用一個ImportBeanDefinitionRegistrar來將設定屬性繫結到Beans上的,這就是ImportBeanDefinitionRegistrar在實踐中的一個實際應用的例子。

點選關注,第一時間瞭解華為雲新鮮技術~