本文分享自华为云社区《Spring高手之路8——Spring Bean模块装配的艺术:@Import详解》,作者: 砖业洋__。
本文将带你深入探索Spring框架的装配机制,以及它如何使你的代码更具模块化和灵活性。我们首先介绍Spring手动装配的基础知识,然后进一步解析@Import注解在模块装配中的关键角色。文章涵盖从导入普通类、配置类,到使用ImportSelector和ImportBeanDefinitionRegistrar进行动态和选择性装配等多个层次,旨在帮助读者全面理解和掌握Spring的装配技术。
1. 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
的使用)通常是更常见和推荐的方式。
2. Spring框架中的模块装配
模块装配就是将我们的类或者组件注册到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
的配置和基于注解的组件扫描来实现模块装配。
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); } }
运行结果如下:
3. @Import模块装配的四种方式
3.1 @Import注解的功能介绍
在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
注解由于其更大的灵活性和控制力,在处理更复杂的场景时,可能会是一个更好的选择。
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.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
的名字默认就是这个类的全限定类名。
运行结果:
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()); } }
运行结果:
在这个例子中,当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()
方法获取全限定类名的方式,比直接硬编码类的全名为字符串更推荐,原因如下:
- 避免错误:如果类名或包名有所改动,硬编码的字符串可能不会跟随变动,这可能导致错误。而使用
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
的自动配置类。
如下图:
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: 这个参数是
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
在实践中的一个实际应用的例子。