Spring 之 @Import 注解使用与源码浅析

1、@Import 的作用?

再说 @Import 之前先回忆下 @Component 的作用,在类上标注该注解,该类就能够被 Spring 扫描封装成 BeanDefinition 并注册到容器中。但现在需要将第三方 jar 包、或者其他路径下面的包中的类也要被扫描注册呢?使用 @Component 处理并不是很友好(加一个 jar 包你都要指定扫描路径太费劲了),所以就有了 @Import,它就可以完美的支持第三方类库的导入,SpringBoot 中就非常喜欢用这种方式,提前把所有的配置类写好组装到一个模块,需要用到的地方直接通过 @Import 导入即可。

2、@Import 四个基本用法

2.1、直接导入实体类

直接导入就是将需要导入的 Apple 类直接写死,如下:


public class Apple {

}

@Import(Apple.class)
public class AtImportTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AtImportTest.class);

		Apple bean = context.getBean(Apple.class);
		System.out.println("bean = " + bean);
	}
}

虽然这种方式最简单,但是有一个弊端,就是当你需要导入很多类的时候,就需要一个一个这样指定太浪费时间了,所以就衍生出下面的几种方式。

2.2、实现 ImportSelector 接口

注意 AppleImportSelector 类上面不要加 @Component 注解,被 @ComponentScan 扫描的话是不会回调到 selectImports() 方法的,这样就不可能导入 Apple 类。


public class Apple {

}

public class AppleImportSelector implements ImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		return new String[]{Apple.class.getName()};
	}
}

@Import(AppleImportSelector.class)
public class AtImportTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AtImportTest.class);

		Object bean = context.getBean(Apple.class);
		System.out.println("bean = " + bean);
	}
}

2.3、实现 DeferredImportSelector 接口

这个接口其实是 ImportSelector 的扩展接口,实现了这个接口导入的类,表示这些导入的类是需要延迟装载的。SpringBoot 自动装配中就用这个类来延迟加载 spring.factories 中配置类,毕竟自动装配是带有 n 多条件的,只有符合某些条件才能够装载的。具体用法如下:

可以实现 DeferredImportSelector 接口,并实现 Group 接口,一般这个 Group 的实现定义为静态嵌套类比较好点,加载和外部类会一起加载。


public class Apple {

}

public class AppleDeferredImportSelector implements DeferredImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {

		return null;
	}

	@Override
	public Class<? extends Group> getImportGroup() {
		return MyInnerGroup.class;
	}

	private static class MyInnerGroup implements Group {

		List<Entry> list = new ArrayList<>();
		AnnotationMetadata metadata = null;

		@Override
		public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
			this.metadata = metadata;
			Entry entry = new Entry(metadata,Apple.class.getName());
			list.add(entry);
		}

		@Override
		public Iterable<Entry> selectImports() {

			return list;
		}
	}
}


@Import(AppleDeferredImportSelector.class)
public class AtImportTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AtImportTest.class);

		Apple bean = context.getBean(Apple.class);
		System.out.println("bean = " + bean);

	}
}

这里需要注意,你实现 Group 接口,其实人家也有一个默认 DeferredImportSelectorGrouping 的组。只不过此时你要实现好 AppleDeferredImportSelector 类的 selectImports() 方法。

2.4、实现 ImportBeanDefinitionRegistrar 接口

直接实现 ImportBeanDefinitionRegistrar 接口,这个接口就更方便了,直接可以让你自己注册 BeanDefinition,代码如下:


public class Apple {

}

public class AppleImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		AbstractBeanDefinition genericBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		genericBeanDefinition.setBeanClass(Apple.class);

		registry.registerBeanDefinition("apple",genericBeanDefinition);
	}
}


@Import(AppleImportBeanDefinitionRegistrar.class)
public class AtImportTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AtImportTest.class);

		Object bean = context.getBean(Apple.class);
		System.out.println("bean = " + bean);

	}
}

但是这里需要注意一个小问题,ImportBeanDefinitionRegistrar 提供了两个方法,如果你将两个方法都重写的话只会生效三个参数的方法,两个参数的方法将不会被调到,除非你手动去调用,后面源码会分析到。

3、源码分析

@Import 最终就是要将类能够加载到 Spring 并封装成 BeanDefinition 注册到容器使命就算告终,现在就开始类分析下 @Import 是怎么将类加载到 Spring 的,那么还是得从老朋友进军 ConfigurationClassPostProcessor(这个类化成骨灰都要认得)。

入口源码如下:

在这里插入图片描述

先看到入参 getImports() ,该方法主要是去收集 @Import 导入的类,源码如下:

在这里插入图片描述

在这里插入图片描述

然后再进入 processImports() 方法内部逻辑,其实 @Import 的基本所有逻辑都在这个方法里面,但是回调方法不是在下面这段代码哦,下面这段代码更多的还是去做收集动作,源码如下:

在这里插入图片描述

解释一下上面这段代码吧,遍历收集完的类,遍历每个类有设么特征,然后执行什么操作?

如果实现了 ImportSelector 接口的立马就去回调该接口方法 selectImports() (可以看见这个接口的执行时序还是非常高的);因为这个执行太及时了,所以在 SpringBoot 自动装配中就不会使用这个类作为导入类。

如果实现了 DeferredImportSelector 接口的,并没有立刻去回调该接口的方法,而是将导入的类保存到了 deferredImportSelectors List 集合中,这里收集保存,那么肯定是准备给后面哪个地方使用。所以 Spring 中就是使用的这个类作为自动装配的导入类,源码如下:

在这里插入图片描述

如果实现了 ImportBeanDefinitionRegistrar 接口的话,也是将导入的类保存到一个 importBeanDefinitionRegistrars Map 集合中,不用想,肯定又是给后面做铺垫,SpringBoot 自动装配类太多,所以也不可能用这个来一个一个手动注入,源码如下:

在这里插入图片描述

如果上述接口都没有实现,那么就会直接走最后的 else 阶段递归调用 processConfigurationClass() 方法,因为谁也不知道你这个类或者类里面有没有其他注解。如果灭有其他注解的话,最终被保存到 configurationClasses Map 集合中。

这里补充一个 ConfigurationClass 类,这个类是个包装类,里面包装了很多的东西,源码如下:

在这里插入图片描述

ImportedBy 集合:主要用来存什么?这样解释下,比如 @Import(Apple.class) public AtImportTest{ } 那么这个集合保存的就是 AtImportTest 类,因为 Apple 是被 AtImportTest 类导入的,或者说一个外部类和内部类的关系,内部类肯定都是有外部类导入的,所以 ImportedBy 保存的就是外部类

beanMethods 集合:主要保存 @Bean 修饰的方法,也只能是方法

importedResources 集合:主要用来保存 @ImportResource 导入的资源类

importBeanDefinitionRegistrars 集合:主要用来保存实现了 ImportBeanDefinitionRegistrar 接口的类

然后 ConfigurationClassParser 类中有个集合,如下:

在这里插入图片描述

configurationClasses 集合:主要用来保存被解析完的类,因为 @Bean、@Import 这些类还没有注册到容器中,暂时保存到 configurationClasses 集合中。

deferredImportSelectorHandler 对象:保存实现了 DeferredImportSelector 接口的类,它里面有个集合 deferredImportSelectors 保存这个类。

补充完之后,继续回到正题,从上面的一序列介绍,都只是讲 @Import 导入的类暂存到 configurationClasses Map 集合中,那么到底什么时候被注册到 Spring 容器中,源码如下:

在这里插入图片描述

从上面源码可以看出,调用实在 parse() 解析完成之后才调的。要知道这个时候其实很多 bean 已经被扫描加载到了 Spring 容器中,进入内部逻辑如下:

在这里插入图片描述

可以看到没有实现任何接口通过 @Import 导入进来的类在第一行就被注册到 Spring 容器中,被 @Bean 修饰的方法在第二行被执行。而实现了 ImportBeanDefinitionRegistrar 接口的类,在最后一行执行,进入源码如下:

在这里插入图片描述

注意这个 registerars 就是前面提到的 importBeanDefinitionRegistrars 集合,保存实现了 ImportBeanDefinitionRegistrar 接口的类。

发现没,这里进来就默认调用 ImportBeanDefinitionRegistrar 接口中有三个参数的方法,然后继续进入内部如下:

在这里插入图片描述

发现自己会手动调用两个参数的方法,所以这里就是为什么上面提到,如果你重写了 ImportBeanDefinitionRegistrar 接口的两个方法的话,就只会回调到三个参数的方法,因为我们重写了 registerBeanDefinitions(三个参数) 方法。

现在还有一个 deferredImportSelectors 集合中保存的东西,在哪里被使用呢?源码如下:

在这里插入图片描述

deferredImportSelectors 集合中就是保存的就是实现了 DeferredImportSelector 接口的类,表示这些类需要被延迟加载,从上面源码可以看出,是在 parse() 完之后执行的,除了需要延迟加载的类,所有的类都已经加载完成。所以为什么 SpringBoot 自动装配中需要将 spring.factories 中的配置通过延迟加载的方式加入到 Spring 容器中,因为自动装配是有 @Condition 条件判断的(比如:@ConditionalOnMissingBean 需要容器中没有这个 bean 才可以注册该 bean),所以自动装配采取的是延迟执行,并且还可以进行分组,分组之后每组成员加载顺序是互补影响的。

进入 process() 核心源码如下:

在这里插入图片描述

获取到实现 DeferredImportSelector 接口的所有类,然后挨个调用 register() 进行注册,看到注册两个字眼,肯定又是把这个元素保存到了某个容器中,然后后面肯定需要用到。然后再调用 processGroupImports() 方法,回调到 DeferredImportSelector 接口方法。源码如下:

在这里插入图片描述

这里有两个地方要注意,groupings 的值从哪里来的呢?configurationClasses 的值又是从哪里来的?

首先看到 groupings 是怎么来的,那么就要回到上面的 register() 注册过程了,源码如下:

在这里插入图片描述

发现 groupings 的值有两种情况:第一种:如果 group 不为 null,那么就使用这个 group 作为值,那么怎么才回让 group 不为 null ?

源码是直接返回 null 的,只要我们能够覆写 getImportGroup() 方法就可以让 group 有不为 null,如下:

在这里插入图片描述

重写之后的如下:

public class AppleDeferredImportSelector implements DeferredImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {

		return new String[]{Apple.class.getName()};
	}

	@Override
	public Class<? extends Group> getImportGroup() {
		return MyInnerGroup.class;
	}

	private static class MyInnerGroup implements Group {

		List<Entry> list = new ArrayList<>();
		AnnotationMetadata metadata = null;

		@Override
		public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
			this.metadata = metadata;
			Entry entry = new Entry(metadata,Apple.class.getName());
			list.add(entry);
		}

		@Override
		public Iterable<Entry> selectImports() {
			
			return list;
		}
	}
}

process() 方法主要用来去收集处理导入进来的类,selectImports() 就是一个结果返回。group 有值了,那么 groupings 就相当于有值了,configurationClasses 从上面源码中也可以看到是保存的触发导入的类。

然后接着继续往下分析,源码如下:

在这里插入图片描述

看到下面这段源码,最终会回调到每个实现了 Group 接口的 process() 和 selectImports() 方法。deferredImports 容器装的是实现了 DeferredImportSelector 接口的类。

在这里插入图片描述

至此对于 @Import 导入类的加载并注册到容器完成。注意此时注册完成说的都是针对 BeanDefinition,并不是真正的实例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值