Spring注解驱动开发(六):利用@Import向容器中注入组件


这是我Spring Frame 专栏的第六篇文章,在 Spring注解驱动开发(五):利用@Lazy实现懒加载 这篇文章中,我向你详细介绍了@Lazy注解的作用以及注意事项,如果你未读过那篇文章,但是对内容感兴趣的话,我建议你去读一下

1. 背景介绍

在 Spring 容器里,有三种常见的向 Ioc 容器中注入组件的方式:

  1. 利用 @ComponentScan+@Component 向容器中注入组件 (专栏第一篇文章讲述)
  2. 利用 @Configuration+@Bean 向容器中注入组件 (专栏第二篇文章讲述)
  3. 利用 @Import 向容器中注入组件
    这三种方式是你使用Spring和阅读 Spring 各功能组件的代码必须要掌握的知识点,本篇文章我就会给你讲述 @Import 的用法
2. @Import 详解

这里我先定义几个自定义组件,以便后续内容的讲述.
这里我按照MVC的开发流程,定义了一套组件,代码如下所示:

  • Person
public class Person {
    private Integer id;
    private String name;
    private String sex;

    public Person() {
    }

    public Person(Integer id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }
	// 省略 getter,setter,toString 方法
  • PersonDao
@Repository
public class PersonDao {
}
  • PersonService
@Service
public class PersonService {
}
  • PersonController
@Controller
public class PersonController {
}
  • 定义配置类 CustomerConfig:
@Configuration
public class CustomerConfig {
}
  • 最后定义一个测试类:
public class IcoMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("Ioc 容器初始化完成");
        System.out.println("---------------------");
        context.close();
    }
}

好啦,至此已经准备好了测试环境,接下来进入文章的正题.

2.1 @Import 概述

按照惯例,我们先看一眼 @Import 注解的源码,并从它的注释了解一下它的作用

/**
 * Indicates one or more <em>component classes</em> to import &mdash; typically
 * {@link Configuration @Configuration} classes.
 *
 * .......
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.0
 * @see Configuration
 * @see ImportSelector
 * @see ImportBeanDefinitionRegistrar
 * @see ImportResource
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
	Class<?>[] value();
}

  1. 从整个类的注释我们可以看到,这个类就是向容器中放入候选Bean的,和@Configuration 类似
  2. 从其属性注释可以知道:@Import可以配合 Configuration、ImportSelector和ImportBeanDefinitionRegistrar 来使用,同时也可以把Import当成普通的bean来使用

好啦,从源码中我们了解了其作用,我们开始尝试使用不同的方式,利用@Import 向容器中注入组件.

2.2 . 利用 @Import 注入普通组件

第一种方式就是向容器中注入普通的组件,不借助其它类(Configuration、ImportSelector和ImportBeanDefinitionRegistrar)
我们先来改造以下配置类,在它的上面标注 @Import 注解,并将Person的类信息设置为其属性值:

@Import(value = {Person.class})
@Configuration
public class CustomerConfig {
}

接下来我们修改一下测试类,打印当前容器中存在的BeanDefinition的名字以及是否可以获取对应的Person对象实例

public class IcoMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("Ioc 容器初始化完成");
        System.out.println("----------BeanDefinitionNames-----------");
        String[] definitionNames = context.getBeanDefinitionNames();
        for (String definitionName : definitionNames) {
            System.out.println(definitionName);
        }
        System.out.println("----------Person对象-----------");
        Person person = context.getBean(Person.class);
        System.out.println("person===>"+person);
        context.close();
    }
}

在这里插入图片描述
从运行结果发现Person的实例被注入到了容器中,说明使用@Import注解导入组件时,容器中就会自动注册这个组件,并且在Spring Ioc容器中的默认名就是组件的全类名
⭐️ 这里我只是注入了一个组件,但是其源码写的是一个数组,你可以批量向容器中导入组件

2.3. 利用 ImportSelector 注入组件

上面我讲述了利用 @Import 可以向容器中注入普通的Bean,那么现在我向你讲述第二种方式:利用 ImportSelector 注入组件
我们还是先看一下ImportSelector 的源码,了解一下它的使用方式:

/**
 * Interface to be implemented by types that determine which @{@link Configuration}
 * class(es) should be imported based on a given selection criteria, usually one or
 * more annotation attributes.
 *
 * ......
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see DeferredImportSelector
 * @see Import
 * @see ImportBeanDefinitionRegistrar
 * @see Configuration
 */
public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 * @return the class names, or an empty array if none
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

	/**
	 * Return a predicate for excluding classes from the import candidates, to be
	 * transitively applied to all classes found through this selector's imports.
	 * <p>If this predicate returns {@code true} for a given fully-qualified
	 * class name, said class will not be considered as an imported configuration
	 * class, bypassing class file loading as well as metadata introspection.
	 * @return the filter predicate for fully-qualified candidate class names
	 * of transitively imported configuration classes, or {@code null} if none
	 * @since 5.2.4
	 */
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}
}

从注释可以看到,这个类主要借助 selectImports()方法,它有一个AnnotationMetadata 类型的参数(能够获取到当前标注@Import注解的类的所有注解信息),它的返回值是一个字符串数组,数组中的每一个元素都应该是想注入组件的全类名

概念很枯燥,接下来我就实定义一个 ImportSelector 实现类,向你展示一下它的用法:

  1. 我们先定义一个实现类
public class CustomerSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 1. 我们看一眼AnnotationMetadata能获取哪些注解
        Set<String> annotationTypes = importingClassMetadata.getAnnotationTypes();
        annotationTypes.forEach(System.out::println);

        // ----------------- 此处你可以自定义一些判断逻辑,来对该类的注解信息进行过滤判断,来指定向导入哪些组件--------------

        // 2. 返回 全类名字符串 数组             !!!! 切记不要返回NULL,否则Spring Ioc 容器会报错
        return new String[]{"org.zhang.blog.ioc.domain.Person"};
    }
}

在里面我先获取了注解信息,这里是让你理解以下 AnnotationMetadata 参数的作用,之后就是返回一个全类名字符串数组
⭐️ 切记不要返回 Null , 否则 Spring 会报错(如果你对错误感兴趣,可以自己 Debug 一下,由于篇幅原因,我就不带你看了)

  1. 修改我们的配置类,在 @Import 注解内只导入这个 ImportSelector 实现类
@Import(value = {CustomerSelector.class})
@Configuration
public class CustomerConfig {
}
  1. 执行我们上面定义好的测试类,查看结果:
public class IcoMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("Ioc 容器初始化完成");
        System.out.println("----------BeanDefinitionNames-----------");
        String[] definitionNames = context.getBeanDefinitionNames();
        for (String definitionName : definitionNames) {
            System.out.println(definitionName);
        }
        System.out.println("----------Person对象-----------");
        Person person = context.getBean(Person.class);
        System.out.println("person===>"+person);
        context.close();
    }
}

在这里插入图片描述

从结果中可以看到:

  • AnnotationMetadata 获取到了 @Import标注的类(CustomerConfig )上面的所有注解信息(@Import,@Configuration)
  • 利用 这个 CustomerSelector 成功的向容器中注入了 Person 类的实例

好啦,相信到这里你应该掌握 ImportSelector 的用法了

2.4 利用 ImportBeanDefinitionRegistrar 注入组件

在上面两部分,我向你介绍了 @Import 注解的两种使用方式,这里我向你介绍最后一种使用方式:自定义 ImportBeanDefinitionRegistrar 实现类
我们还是先看一下 ImportBeanDefinitionRegistrar 的源码,了解一下它的使用方式:

/**
 * Interface to be implemented by types that register additional bean definitions when
 * processing @{@link Configuration} classes. Useful when operating at the bean definition
 * level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
 *
 * ......
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see Import
 * @see ImportSelector
 * @see Configuration
 */
public interface ImportBeanDefinitionRegistrar {

	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {

		registerBeanDefinitions(importingClassMetadata, registry);
	}

	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}
}

从源码中可以看到,它有一个 registerBeanDefinitions() 方法,通过该方法,我们可以向Spring容器中注册 BeanDefinition (Bean的元信息,在创建组件的时候需要这些元信息),Spring官方在动态注册bean时,大部分使用的都是ImportBeanDefinitionRegistrar接口

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;

}

上面代码开启 Spring AOP 功能时就利用了 ImportBeanDefinitionRegistrar

好啦,概念说完了,我们开始实操使用一下它,这里我们只要重写 registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法就好了,它的第一个参数我相信你已经不陌生了,第二个参数 BeanDefinitionRegistry 的作用和它的名字一样: BeanDefiniton 的注册类,将对应Bean元信息放入Ioc容器中,在之后创建组件实例时使用

  1. 我们先定义一个ImportBeanDefinitionRegistrar
public class CustomerImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 1. 这里你依然可以利用AnnotationMetadata和BeanDefinitionRegistry 中的 的信息定制化的注册组件
        String[] beanDefinitionNames = registry.getBeanDefinitionNames();
        System.out.println("-----------BeanDefinitionRegistry 中的 BeanDefinition 信息--------------");
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
        System.out.println("----------注入Person 的 BeanDefinition------------");
        RootBeanDefinition beanDefinition = new RootBeanDefinition(Person.class);
        // 注册一个bean,并且指定bean的名称
        registry.registerBeanDefinition("person", beanDefinition);
    }
}
  1. 修改配置类,利用 @Import 导入 CustomerImportBeanDefinitionRegistrar
@Import(value = {CustomerImportBeanDefinitionRegistrar.class})
@Configuration
public class CustomerConfig {

    /**
     * 向容器中注入Person1
     * @return
     */
    public Person person1() {
        System.out.println("person1 被加载");
        return new Person(1, "jack", "female");
    }
}
  1. 执行测试类:
public class IcoMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);
        System.out.println("Ioc 容器初始化完成");
        System.out.println("----------BeanDefinitionNames-----------");
        String[] definitionNames = context.getBeanDefinitionNames();
        for (String definitionName : definitionNames) {
            System.out.println(definitionName);
        }
        System.out.println("----------Person对象-----------");
        Person person = context.getBean(Person.class);
        System.out.println("person===>"+person);
        context.close();
    }
}

在这里插入图片描述
在注册和未注册Person前后的Ioc容器中的 BeanDefinition 是不同的,这也侧面验证了这个Person的 BeanDefinition 是利用CustomerImportBeanDefinitionRegistrar 注入的

好啦,到这里我相信你已经可以基本使用 ImportBeanDefinitionRegistrar 向容器中注入 Bean 了

3. 扩展介绍
3.1 Spring 扩展原理实现

我个人认为,@Import 是 Spring Ioc 容器提供的最佳的扩展功能,这里我给你一些理由,在你以后使用SpringBoot以及Spring 的各种扩展功能时都会看到它的踪迹:

  • ImportSelecto是Spring中导入外部配置的核心接口,在Spring Boot的自动化配置@EnableXXX(功能性注解)都有它的存在
  • ImportBeanDefinitionRegistrar 也是Spring 常用功能的注册Bean的不二之选,它可以独立的向Spring Ioc 容器中导入一些BeanDefinition信息
3.2 ImportBeanDefinitionRegistrar 原理分析

ImportBeanDefinitionRegistrar 接口的所有实现类都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中动态注册的bean是优先于依赖其的bean初始化的,也能被aop、validator等机制处理

在此处,我就带你探究一下如何从源码中追溯到 ConfigurationClassPostProcessor 处理这些类信息:

  1. 我们现在自定义的接口实现类的registerBeanDefinitions打一个断点
    在这里插入图片描述

  2. Debug 模式运行我们的测试类,看一下对应的函数调用栈:
    在这里插入图片描述
    从调用栈明显可以看到是从ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry 开始处理 BeanDefinitionRegistry
    调用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitions 开始加载BeanDefinition
    之后调用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass开始加载所有配置类(此时会顺便处理@Import注解相关信息)
    最后调用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsFromRegistrars开始遍历所有的 ImportBeanDefinitionRegistrar接口实现类

	private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
		registrars.forEach((registrar, metadata) ->
				registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
	}

到这里,你应该能知道 ImportBeanDefinitionRegistrar 在何时起作用了

4. 总结

这篇文章,我主要向你介绍了:

  • Spring Ioc 容器注入组件的三种常见方式
  • @Import 的用法
  • ImportBeanDefinitionRegistrar 的源码调用时机

最后,我希望你看完本篇文章后,能够在适当的时候使用 @Import 注解向容器中注入组件,也希望你指出我在文章中的错误点,希望我们一起进步,也希望你能给我的文章点个赞,原创不易!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值