Spring 高手之路 8——Spring Bean 模块装配的艺术:@Import 详解

1. Spring 手动装配基础
  在Spring中,手动装配通常是指通过XML配置文件明确指定Bean及其依赖,或者在代码中直接使用new关键字创建对象并设定依赖关系。
  然而,随着Spring 2.0引入注解,以及Spring 3.0全面支持注解驱动开发,这个过程变得更加自动化。例如,通过使用@Component + @ComponentScan,Spring可以自动地找到并创建bean,通过@Autowired,Spring可以自动地注入依赖。这种方式被称为 "自动装配"。
  对于手动装配,最常见的场景可能是在不使用Spring的上下文的单元测试或者简单的POJO类中,通过new关键字直接创建对象和设定依赖关系。比如下面这段代码:
  在这个例子中,我们显式地创建了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
LibraryConfig.java
  使用 @Import 注解来导入一个普通的类(即一个没有使用 @Component 或者 @Service 之类的注解标记的类),Spring 会为该类创建一个 Bean,并且这个 Bean 的名字默认就是这个类的全限定类名。
主程序:
运行结果如下:
 
 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注解,通过此注解我们将把所有相关的类装配到图书馆里面:
  这个@ImportLibrary注解内部实际上使用了@Import注解。当Spring处理@Import注解时,会将其参数指定的类添加到Spring应用上下文中。当我们在Library类上使用@ImportLibrary注解时,Spring会将Librarian.class、Book.class和BookShelf.class这三个类添加到应用上下文中。
  然后,我们创建图书馆管理员(Librarian)、图书(Book)、书架(BookShelf)这三个类:
Librarian.java
Book.java
BookShelf.java
最后,我们创建一个图书馆(Library)类,并在这个类上使用我们刚刚创建的@ImportLibrary注解:
然后我们可以创建一个启动类并初始化IOC容器,看看是否可以成功获取到Librarian类、BookShelf类和Book类的实例:
这个例子中,我们通过@Import注解一次性把Librarian、Book和BookShelf这三个类导入到了Spring的IOC容器中,这就是模块装配的强大之处。
调试结果
 
 当我们使用 @Import 注解来导入一个普通的类(即一个没有使用 @Component 或者 @Service 之类的注解标记的类),Spring 会为该类创建一个 Bean,并且这个 Bean 的名字默认就是这个类的全限定类名。
运行结果:
 
 3.3 导入配置类的策略
这里使用Spring的 @Import注解导入配置类,我们将创建一个BookConfig类和LibraryConfig类,然后在主应用类中获取Book实例。
全部代码如下:
创建一个配置类BookConfig,用于创建和配置Book实例:
在这里,我们定义了一个Book类:
创建一个配置类LibraryConfig,使用@Import注解来导入BookConfig类:
主程序:
运行结果:
 
 在这个例子中,当Spring容器启动时,它会通过@Import注解将BookConfig类导入到Spring 上下文中,并创建一个Bean。然后我们可以在主程序中通过context.getBean(Book.class)获取到Book的实例,并打印出书名。
3.4 使用 ImportSelector 进行选择性装配
如果我们想动态地选择要导入的类,我们可以使用一个ImportSelector实现。
全部代码如下:
定义一个 Book 类:
创建图书馆管理员Librarian类
定义一个 BookImportSelector,实现 ImportSelector 接口:
  ImportSelector接口可以在运行时动态地选择需要导入的类。实现该接口的类需要实现selectImports方法,这个方法返回一个字符串数组,数组中的每个字符串代表需要导入的类的全类名,我们可以直接在这里将 Book 类和 Librarian 类加入到了 Spring 容器中。
使用Class.getName()方法获取全限定类名的方式,比直接硬编码类的全名为字符串更推荐,原因如下:
- 避免错误:如果类名或包名有所改动,硬编码的字符串可能不会跟随变动,这可能导致错误。而使用 - Class.getName()方法,则会随类的改动自动更新,避免此类错误。
- 代码清晰:使用 - Class.getName()能让读代码的人更清楚地知道你是要引用哪一个类。
- 增强代码的可读性和可维护性:使用类的字节码获取全限定类名,使得代码阅读者可以清晰地知道这是什么类,增加了代码的可读性。同时,也方便了代码的维护,因为在修改类名或者包名时,不需要手动去修改硬编码的类名。 
定义一个配置类 LibraryConfig,使用 @Import 注解导入 BookImportSelector:
创建一个主应用类,从 Spring 的AnnotationConfigApplicationContext 中获取 Book 的实例:
运行结果:
 
   在 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 类:
定义一个 BookRegistrar 类,实现 ImportBeanDefinitionRegistrar 接口:
下面来详细解释一下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:
创建一个主应用类,从 Spring ApplicationContext 中获取 Book 的实例:
运行结果:
 
   在这个例子中,我们使用 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-----------------------
版权声明: 本文为 InfoQ 作者【砖业洋__】的原创文章。
原文链接:【http://xie.infoq.cn/article/3d1c43ec4d214084ded3562d6】。文章转载请联系作者。












 
    
评论