文章目录
1. Spring手动装配基础
在
Spring
中,手动装配通常是指通过
XML
配置文件明确指定
Bean
及其依赖,或者在代码中直接使用
new
关键字创建对象并设定依赖关系。
然而,随着
Spring 2.0
引入注解,以及
Spring 3.0
全面支持注解驱动开发,这个过程变得更加自动化。例如,通过使用
@Component + @ComponentScan
,
Spring
可以自动地找到并创建
bean
,通过
@Autowired
,
Spring
可以自动地注入依赖。这种方式被称为 “自动装配”。
对于手动装配,最常见的场景可能是在不使用
Spring
的上下文的单元测试或者简单的
POJO
类中,通过
new
关键字直接创建对象和设定依赖关系。比如下面这段代码:
publicclassMain{publicstaticvoidmain(String[] args){ServiceA serviceA =newServiceA();ServiceB serviceB =newServiceB(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
packagecom.example.demo.bean;publicclassBook{privateString name;publicBook(){this.name ="Imported Book";}publicStringgetName(){return name;}@OverridepublicStringtoString(){return"Book{"+"name='"+ name +'\''+'}';}}
LibraryConfig.java
packagecom.example.demo.configuration;importcom.example.demo.bean.Book;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Import;@Configuration@Import(Book.class)publicclassLibraryConfig{}
使用
@Import
注解来导入一个普通的类(即一个没有使用
@Component
或者
@Service
之类的注解标记的类),
Spring
会为该类创建一个
Bean
,并且这个
Bean
的名字默认就是这个类的全限定类名。
主程序:
packagecom.example.demo;importcom.example.demo.bean.Book;importcom.example.demo.configuration.LibraryConfig;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassDemoApplication{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext context =newAnnotationConfigApplicationContext(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@interfaceImportLibrary{}
这个
@ImportLibrary
注解内部实际上使用了
@Import
注解。当
Spring
处理
@Import
注解时,会将其参数指定的类添加到
Spring
应用上下文中。当我们在
Library
类上使用
@ImportLibrary
注解时,
Spring
会将
Librarian.class
、
Book.class
和
BookShelf.class
这三个类添加到应用上下文中。
然后,我们创建图书馆管理员(
Librarian
)、图书(
Book
)、书架(
BookShelf
)这三个类:
Librarian.java
packagecom.example.demo.bean;publicclassLibrarian{publicvoidmanage(){System.out.println("The librarian is managing the library.");}}
Book.java
packagecom.example.demo.bean;publicclassBook{privateString name;// @ImportLibrary里面有@Import会自动装配,会调用无参构造,不写会报错publicBook(){}publicBook(String name){this.name = name;}publicStringgetName(){return name;}}
BookShelf.java
packagecom.example.demo.bean;importjava.util.List;publicclassBookShelf{privateList<Book> books;// @ImportLibrary里面有@Import会自动装配,会调用无参构造,不写会报错publicBookShelf(){}publicBookShelf(List<Book> books){this.books = books;}publicList<Book>getBooks(){return books;}}
最后,我们创建一个图书馆(
Library
)类,并在这个类上使用我们刚刚创建的
@ImportLibrary
注解:
packagecom.example.demo.configuration;importcom.example.demo.annotations.ImportLibrary;importcom.example.demo.bean.Book;importcom.example.demo.bean.BookShelf;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.Arrays;@ImportLibrary@ConfigurationpublicclassLibrary{@BeanpublicBookbook1(){returnnewBook("The Catcher in the Rye");}@BeanpublicBookbook2(){returnnewBook("To Kill a Mockingbird");}@BeanpublicBookShelfbookShelf(Book book1,Book book2){returnnewBookShelf(Arrays.asList(book1, book2));}}
然后我们可以创建一个启动类并初始化
IOC
容器,看看是否可以成功获取到
Librarian
类、
BookShelf
类和
Book
类的实例:
packagecom.example.demo;importcom.example.demo.bean.Book;importcom.example.demo.bean.BookShelf;importcom.example.demo.bean.Librarian;importcom.example.demo.configuration.Library;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassDemoApplication{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext context =newAnnotationConfigApplicationContext(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
实例:
packagecom.example.demo.configuration;importcom.example.demo.bean.Book;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassBookConfig{@BeanpublicBookbook(){Book book =newBook();
book.setName("Imported Book");return book;}}
在这里,我们定义了一个
Book
类:
packagecom.example.demo.bean;publicclassBook{privateString name;publicvoidsetName(String name){this.name = name;}publicStringgetName(){return name;}}
创建一个配置类
LibraryConfig
,使用
@Import
注解来导入
BookConfig
类:
packagecom.example.demo.configuration;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Import;@Configuration@Import(BookConfig.class)publicclassLibraryConfig{}
主程序:
packagecom.example.demo;importcom.example.demo.bean.Book;importcom.example.demo.configuration.LibraryConfig;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassDemoApplication{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext context =newAnnotationConfigApplicationContext(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
类:
packagecom.example.demo.bean;publicclassBook{privateString name ="java从入门到精通";publicvoidsetName(String name){this.name = name;}publicStringgetName(){return name;}}
创建图书馆管理员
Librarian
类
packagecom.example.demo.bean;publicclassLibrarian{publicvoidmanage(){System.out.println("The librarian is managing the library.");}}
定义一个
BookImportSelector
,实现
ImportSelector
接口:
packagecom.example.demo.configuration;importcom.example.demo.bean.Librarian;importorg.springframework.context.annotation.ImportSelector;importorg.springframework.core.type.AnnotationMetadata;publicclassBookImportSelectorimplementsImportSelector{/**
* 这里示范2种方式,一种是拿到class文件后getName,一种是直接写全限定类名
* @param importingClassMetadata
* @return
*/@OverridepublicString[]selectImports(AnnotationMetadata importingClassMetadata){returnnewString[]{"com.example.demo.bean.Book",Librarian.class.getName()};}}
ImportSelector
接口可以在运行时动态地选择需要导入的类。实现该接口的类需要实现
selectImports
方法,这个方法返回一个字符串数组,数组中的每个字符串代表需要导入的类的全类名,我们可以直接在这里将
Book
类和
Librarian
类加入到了
Spring
容器中。
使用
Class.getName()
方法获取全限定类名的方式,比直接硬编码类的全名为字符串更推荐,原因如下:
- 避免错误:如果类名或包名有所改动,硬编码的字符串可能不会跟随变动,这可能导致错误。而使用
Class.getName()
方法,则会随类的改动自动更新,避免此类错误。 - 代码清晰:使用
Class.getName()
能让读代码的人更清楚地知道你是要引用哪一个类。 - 增强代码的可读性和可维护性:使用类的字节码获取全限定类名,使得代码阅读者可以清晰地知道这是什么类,增加了代码的可读性。同时,也方便了代码的维护,因为在修改类名或者包名时,不需要手动去修改硬编码的类名。
定义一个配置类
LibraryConfig
,使用
@Import
注解导入
BookImportSelector
:
packagecom.example.demo.configuration;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Import;@Configuration@Import(BookImportSelector.class)publicclassLibraryConfig{}
创建一个主应用类,从
Spring
的
AnnotationConfigApplicationContext
中获取
Book
的实例:
packagecom.example.demo;importcom.example.demo.bean.Book;importcom.example.demo.bean.Librarian;importcom.example.demo.configuration.LibraryConfig;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassDemoApplication{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext context =newAnnotationConfigApplicationContext(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
类:
packagecom.example.demo.bean;publicclassBook{privateString name ="java从入门到精通";publicvoidsetName(String name){this.name = name;}publicStringgetName(){return name;}}
定义一个
BookRegistrar
类,实现
ImportBeanDefinitionRegistrar
接口:
packagecom.example.demo.configuration;importcom.example.demo.bean.Book;importorg.springframework.beans.factory.config.BeanDefinition;importorg.springframework.beans.factory.support.BeanDefinitionBuilder;importorg.springframework.beans.factory.support.BeanDefinitionRegistry;importorg.springframework.context.annotation.ImportBeanDefinitionRegistrar;importorg.springframework.core.type.AnnotationMetadata;publicclassBookRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(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
:
packagecom.example.demo.configuration;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Import;@Configuration@Import(BookRegistrar.class)publicclassLibraryConfig{}
创建一个主应用类,从
Spring ApplicationContext
中获取
Book
的实例:
packagecom.example.demo;importcom.example.demo.bean.Book;importcom.example.demo.configuration.LibraryConfig;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassDemoApplication{publicstaticvoidmain(String[] args){AnnotationConfigApplicationContext context =newAnnotationConfigApplicationContext(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
在实践中的一个实际应用的例子。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
版权归原作者 砖业洋__ 所有, 如有侵权,请联系我们删除。