本文分享自華為雲社群《Spring高手之路8——Spring Bean模組裝配的藝術:@Import詳解》,作者: 磚業洋__。
本文將帶你深入探索Spring框架的裝配機制,以及它如何使你的程式碼更具模組化和靈活性。我們首先介紹Spring手動裝配的基礎知識,然後進一步解析@Import註解在模組裝配中的關鍵角色。文章涵蓋從匯入普通類、設定類,到使用ImportSelector和ImportBeanDefinitionRegistrar進行動態和選擇性裝配等多個層次,旨在幫助讀者全面理解和掌握Spring的裝配技術。
在Spring
中,手動裝配通常是指通過XML
組態檔明確指定Bean
及其依賴,或者在程式碼中直接使用new
關鍵字建立物件並設定依賴關係。
然而,隨著Spring 2.0
引入註解,以及Spring 3.0
全面支援註解驅動開發,這個過程變得更加自動化。例如,通過使用@Component + @ComponentScan
,Spring
可以自動地找到並建立bean
,通過@Autowired
,Spring
可以自動地注入依賴。這種方式被稱為 「自動裝配」。
對於手動裝配,最常見的場景可能是在不使用Spring
的上下文的單元測試或者簡單的POJO
類中,通過new
關鍵字直接建立物件和設定依賴關係。比如下面這段程式碼:
public class Main { public static void main(String[] args) { ServiceA serviceA = new ServiceA(); ServiceB serviceB = new ServiceB(serviceA); //... } }
在這個例子中,我們顯式地建立了ServiceA
和ServiceB
的物件,並將ServiceA
的物件作為依賴傳遞給了ServiceB
。這就是一個典型的手動裝配的例子。
需要注意的是,手動裝配的使用通常是有限的,因為它需要開發者顯式地在程式碼中管理物件的建立和依賴關係,這在大型應用中可能會變得非常複雜和難以管理。因此,Spring
的自動裝配機制(例如@Autowired
註解,或者@Configuration
和@Bean
的使用)通常是更常見和推薦的方式。
模組裝配就是將我們的類或者元件註冊到Spring
的IoC
(Inversion of Control
,控制反轉)容器中,以便於Spring
能夠管理這些類,並且在需要的時候能夠為我們自動地將它們注入到其他的元件中。
在Spring
框架中,有多種方式可以實現模組裝配,包括:
基於Java的設定:通過使用@Configuration
和@Bean
註解在Java
程式碼中定義的Bean
。這是一種宣告式的方式,我們可以明確地控制Bean
的建立過程,也可以使用@Value
和@PropertySource
等註解來處理設定屬性。
基於XML的設定:Spring
也支援通過XML
組態檔定義Bean
,這種方式在早期的Spring
版本中更常見,但現在基於Java
的設定方式更為主流。
基於註解的元件掃描:通過使用@Component
、@Service
、@Repository
、@Controller
等註解以及@ComponentScan
來自動檢測和註冊Bean
。這是一種隱式的方式,Spring
會自動掃描指定的包來查詢帶有這些註解的類,並將這些類註冊為Bean
。
使用@Import:這是一種顯式的方式,可以通過它直接註冊類到IOC
容器中,無需這些類帶有@Component
或其他特殊註解。我們可以使用它來註冊普通的類,或者註冊實現了ImportSelector
或ImportBeanDefinitionRegistrar
介面的類,以提供更高階的裝配能力。
每種方式都有其應用場景,根據具體的需求,我們可以選擇合適的方式來實現模組裝配。比如在Spring Boot
中,我們日常開發可能會更多地使用基於Java
的設定和基於註解的元件掃描來實現模組裝配。
@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); } }
執行結果如下:
在Spring
中,有時候我們需要將某個類(可能是一個普通類,可能是一個設定類等等)匯入到我們的應用程式中。Spring
提供了四種主要的方式來完成這個任務,後面我們會分別解釋。
@Import
註解可以有以下幾種使用方式:
匯入普通類:可以將普通類(沒有被@Component
或者@Service
等註解標註的類)匯入到Spring
的IOC
容器中,Spring
會為這個類建立一個Bean
,這個Bean
的名字預設為類的全限定類名。
匯入設定類:可以將一個或多個設定類(被@Configuration
註解標註的類)匯入到Spring
的IOC
容器中,這樣我們就可以一次性地將這個設定類中定義的所有Bean
匯入到Spring
的IOC
容器中。
使用ImportSelector介面:如果想動態地匯入一些Bean
到Spring
的IOC
容器中,那麼可以實現ImportSelector
介面,然後在@Import
註解中引入ImportSelector
實現類,這樣Spring
就會將ImportSelector
實現類返回的類匯入到Spring
的IOC
容器中。
使用ImportBeanDefinitionRegistrar介面:如果想在執行時動態地註冊一些Bean
到Spring
的IOC
容器中,那麼可以實現ImportBeanDefinitionRegistrar
介面,然後在@Import
註解中引入ImportBeanDefinitionRegistrar
實現類,這樣Spring
就會將ImportBeanDefinitionRegistrar
實現類註冊的Bean
匯入到Spring
的IOC
容器中。
@Import
註解主要用於手動裝配,它可以讓我們顯式地匯入特定的類或者其他設定類到Spring
的IOC
容器中。特別是當我們需要引入第三方庫中的類,或者我們想要顯式地控制哪些類被裝配進Spring
的IOC
容器時,@Import
註解會非常有用。它不僅可以直接匯入普通的 Java
類並將其註冊為 Bean
,還可以匯入實現了 ImportSelector
或 ImportBeanDefinitionRegistrar
介面的類。這兩個介面提供了更多的靈活性和控制力,使得我們可以在執行時動態地註冊 Bean
,這是通過 @Configuration + @Bean
註解組合無法做到的。
例如,通過 ImportSelector
介面,可以在執行時決定需要匯入哪些類。而通過 ImportBeanDefinitionRegistrar
介面,可以在執行時控制 Bean
的定義,包括 Bean
的名稱、作用域、構造引數等等。
雖然 @Configuration + @Bean
在許多情況下都足夠使用,但 @Import
註解由於其更大的靈活性和控制力,在處理更復雜的場景時,可能會是一個更好的選擇。
我們第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.class
、Book.class
和BookShelf.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
註解一次性把Librarian
、Book
和BookShelf
這三個類匯入到了Spring
的IOC
容器中,這就是模組裝配的強大之處。
偵錯結果
當我們使用 @Import
註解來匯入一個普通的類(即一個沒有使用 @Component
或者 @Service
之類的註解標記的類),Spring
會為該類建立一個 Bean
,並且這個 Bean
的名字預設就是這個類的全限定類名。
執行結果:
這裡使用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()); } }
執行結果:
在這個例子中,當Spring
容器啟動時,它會通過@Import
註解將BookConfig
類匯入到Spring
上下文中,並建立一個Bean
。然後我們可以在主程式中通過context.getBean(Book.class)
獲取到Book
的範例,並列印出書名。
如果我們想動態地選擇要匯入的類,我們可以使用一個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()
方法獲取全限定類名的方式,比直接寫死類的全名為字串更推薦,原因如下:
Class.getName()
方法,則會隨類的改動自動更新,避免此類錯誤。Class.getName()
能讓讀程式碼的人更清楚地知道你是要參照哪一個類。定義一個設定類 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(); } }
執行結果:
在 Spring Boot
中,ImportSelector
被大量使用,尤其在自動設定(auto-configuration
)機制中起著關鍵作用。例如,AutoConfigurationImportSelector
類就是間接實現了 ImportSelector
,用於自動匯入所有 Spring Boot
的自動設定類。
我們通常會在Spring Boot
啟動類上使用 @SpringBootApplication
註解,實際上,@SpringBootApplication
註解中也包含了 @EnableAutoConfiguration
,@EnableAutoConfiguration
是一個複合註解,它的實現中匯入了普通類 @Import(AutoConfigurationImportSelector.class)
,AutoConfigurationImportSelector
類間接實現了 ImportSelector
介面,用於自動匯入所有 Spring Boot
的自動設定類。
如下圖:
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: 這個引數是Spring
的Bean
定義註冊類,我們可以通過它往Spring
容器中註冊Bean
。在這裡,我們使用它來註冊我們的Book Bean
。
在方法registerBeanDefinitions
中,我們建立了一個BeanDefinition
,並將其註冊到Spring
的BeanDefinitionRegistry
中。
程式碼首先通過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()); } }
執行結果:
在這個例子中,我們使用 AnnotationConfigApplicationContext
初始化 Spring
容器並提供設定類。然後通過 context.getBean("book", Book.class)
從 Spring
容器中獲取名為 book
的範例。
ImportBeanDefinitionRegistrar
介面提供了非常大的靈活性,我們可以根據自己的需求編寫任何需要的註冊邏輯。這對於構建複雜的、高度客製化的Spring應用是非常有用的。
Spring Boot
就廣泛地使用了ImportBeanDefinitionRegistrar
。例如,它的@EnableConfigurationProperties
註解就是通過使用一個ImportBeanDefinitionRegistrar
來將設定屬性繫結到Beans
上的,這就是ImportBeanDefinitionRegistrar
在實踐中的一個實際應用的例子。