最近在琢磨为啥spring可以很优雅、很容易地并进其他项目的原理。刚好与springboot的自动装配有点关系,特此记录下。
说起自动装配,在SpringBoot中,通常会使用一个@Enable....
的注解进行加入其他项目源码。比如常见的@EnableAutoConfiguration
、还有分布式中的@EnableDiscoveryClient
、@EnableFeign
等等。
其中都有@Import
的使用,说到这里,这个注解主要是做什么用的呢?
如下例子所示,创建一个bean类,其中创造一个方法。
public class MyDemo1 {
public void test() {
System.out.println("MyDemo1。。。。。");
}
}
创建一个测试类,注意该测试类可以被spring扫描到。本次使用AnnotationConfigApplicationContext
进行验证,如下所示:
import cn.xj.bean1.MyDemo1;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyDemo1.class) // import 注入某个对象
public class Test1 {
public static void main(String[] args) {
// 注解扫描对应的bean
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Test1.class);
MyDemo1 bean = applicationContext.getBean(MyDemo1.class);
// 调用对象的方法
bean.test();
}
}
还是上面的那个MyDemo1
,本次换一个方法进行bean的注入
实现 ImportSelector 并重写 selectImports
如下所示:
import cn.xj.bean1.MyDemo1;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyDemoConfig2 implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{MyDemo1.class.getName()};
}
}
创建一个配置类,将这个实现 ImportSelector 并重写 selectImports
的类进行载入。
import cn.xj.bean1.MyDemo1;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyDemoConfig2.class) // 导入的是一个实现 ImportSelector 并重写 selectImports 的类
public class MyDemoConfigTest2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MyDemoConfigTest2.class);
MyDemo1 bean = applicationContext.getBean(MyDemo1.class);
bean.test();
}
}
在源码中,针对@ImportSelector
的说明如下
依旧还是上面这个MyDemo1
类,这次配置类用@ImportSelector
的变种ImportBeanDefinitionRegistrar
,代码如下所示:
import cn.xj.bean1.MyDemo1;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class MyDemoConfig3 implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition bd = new RootBeanDefinition();
bd.setBeanClass(MyDemo1.class);
registry.registerBeanDefinition("MyDemo1", bd); // 对这个bean起一个别名
}
}
然后再将这个类,采取@Import
的方式进行载入。如下所示:
import cn.xj.bean1.MyDemo1;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyDemoConfig3.class)
public class MyDemoConfigTest3 {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MyDemoConfigTest3.class);
MyDemo1 bean = (MyDemo1) applicationContext.getBean("MyDemo1"); // 因为是别名注入,所以采取别名获取bean
bean.test();
}
}
在spring的源码中,对于类创建
实例于容器中,采取的是IOC
控制反转的思想,其主要的逻辑流程如下所示:
spring在启动时,会加载xml配置文件
、config 类注解扫描
、import注解
等方式进行bean的扫描、创建和载入容器。假设现在有一个另外的框架,springboot能够很简单的将其并入其中,并进行其他框架bean的加载项,配置项的加载项等。如:分布式微服务模块嵌入feign。
在本篇文章@Import
的描述中,就说到一个东西。
@Enable....
实现其他框架的载入。
其原理也很简单,就拿@SpringBootApplication
注解来说,springboot在启动的时候,就会去识别启动类上的各项注解信息,其中会有一个@EnableAutoConfiguration
的注解,点击进入后,可以发现其实是一个实现 DeferredImportSelector 接口的 类
。
上文就说到
@Import
可以将一个类加载至spring容器中
观察AutoConfigurationImportSelector
类,可以发现,他的核心代码就是重写了DeferredImportSelector
中的两个方法,分别是selectImports
与getImportGroup
。
DeferredImportSelector
是ImportSelector
的子类。
相对ImportSelector
做了额外的扩展,最主要的就是getImportGroup()
组的概念。
【疑问】
这两个方法的作用是什么?
回答这个问题之前,首先需要需要知道
ImportSelector
提供的方式为什么不可行。
在上面的验证中,一个类实现ImportSelector
并重写selectImports
后,在其中返回需要加载的类的全路径
数组信息。的确可以进行一个bean的创建与注入容器中。但是紧接着有一个更严重的问题产生。比如下面这种情况:
springboot-jdbc中,默认就会有一个数据库的连接池配置。
但是在实际开发中,通常的配置项采取的是自定义的配置。
如果使用的是ImportSelector
进行类的加载与创建,项目框架怎么选择自定义的连接池还是默认的连接池作为首要的配置呢?
【扩充】
知识点:
框架本身就有一个默认的配置类,如果开发者进行了自定义的配置,那么在项目中使用的是自定义的配置项,默认的并未进行加载。
【扩充】
知识点:
也可能有人会说,在spring的注解中,有一个
@ConditionalOnClass
的注解,也能实现类似的功能。
如:@ConditionalOnBean(MyDemo2.class)
表示如果存在一个MyDemo2
的bean,则不会加载@ConditionalOnBean
修饰的bean
但是针对这种问题,如果spring依旧还是使用ImportSelector
接口的实现类进行大批量的指定bean的加载操作,则可能出现两个bean同时加载的问题。
spring针对bean的加载顺序,并不能保证。
如果先加载@ConditionalOnBean 修饰的bean
,会判断无指定的bean,则会进行创建;
导致后执行的bean也被加载进入容器。
此时,就需要使用到一个ImportSelector
的变种配置类DeferredImportSelector
。追加了一个分组
的概念。
编写一个测试类,该类实现DeferredImportSelector
并重写selectImports
与getImportGroup
方法,如下所示:
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.util.ArrayList;
import java.util.List;
/**
* spring在解析 bean定义 时,会通过该方法判断是否 getImportGroup() 有返回值
*/
public class MyDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"cn.xj.bean2.MyDemo1"};
}
/**
* 如果返回的是null,则会直接调用 cn.xj.test.MyDeferredImportSelector#selectImports 加载对应的bean;
*
* 如果返回的是一个自定义的 group 对象,则会加载自定义对象中的 selectImports 进行加载
* @return
*/
@Override
public Class<? extends Group> getImportGroup() {
return MyGroup.class;
}
/**
* 自定义 group
*/
private static class MyGroup implements DeferredImportSelector.Group{
AnnotationMetadata metadata;
@Override
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
this.metadata = metadata;
}
@Override
public Iterable<Entry> selectImports() {
List<Entry> lists = new ArrayList<>();
lists.add(new Entry(this.metadata,"cn.xj.bean2.MyDemo2"));
return lists;
}
}
}
创建一个测试类,使用@Import
进行类的扫描与注入。逻辑如下所示:
import cn.xj.bean1.MyDemo1;
import cn.xj.bean2.MyDemo2;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Import;
@Import(MyDeferredImportSelector.class)
public class MyDeferredImportSelectorTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MyDeferredImportSelectorTest.class);
MyDemo2 bean2 = applicationContext.getBean(MyDemo2.class);
bean2.test();
// 异常测试 看拿不拿得到 MyDemo1 的实例
MyDemo1 bean1 = applicationContext.getBean(MyDemo1.class);
bean1.test();
}
}
如果重写了
getImportGroup()
,并返回了一个自定义的 Group 类
。
在spring解析bean定义类
时,会先进行getImportGroup
的返回值判断。
如果返回的是一个null
,则直接将cn.xj.test.MyDeferredImportSelector#selectImports
的返回值进行解析。
如果返回的事一个自定义的group
,则会直接去cn.xj.test.MyDeferredImportSelector.MyGroup#selectImports
进行bean的解析。