在探究springboot默认注解扫描的过程中发现,在ConfigurationClassParser中除了对组件扫描进行处理,还对@PropertySource、@Import、@ImportResource、@Bean等注解进行处理。
下面来看看@Import注解的作用和它的源码。
- 参考视频:https://www.bilibili.com/video/BV1Bq4y1Q7GZ?p=6
- 通过视频的学习和自身的理解整理出的笔记。
一、前期准备
1.1 创建工程
创建springboot项目,springboot版本为2.5.0,引入spring-boot-starter-web依赖,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 创建文件
🔶 创建一个简单的Controller用于测试
com/springboot/controller/HelloController
@RestController
public class HelloController {
public void helloController() {
System.out.println("创建了");
}
@RequestMapping("hello")
public String hello() {
return "hello";
}
}
🔶 创建一个配置类,用于扫描com包下的文件
com/test1/config/Config
@Configuration
@ComponentScan("com")
public class Config {
}
🔶 创建一个简单的Service用于测试
com/test1/service/UserService
@Component
public class UserService {
}
🔶 修改启动类
@Import(Config.class)
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootApplication.class, args);
System.out.println(run.getBean(UserService.class));
}
}
从ConfigurableApplicationContext
的名字可以看出来,它是一个容器(ApplicationContext),所以我们可以通过这个容器来获取bean对象,这里就获取刚刚创建的UserService。
1.3 说明
为了使用@Import注解的方式加载配置类,项目结构设置如下。
可以看出service和config并不在启动类的同级目录下。这就说明启动类并不会去自动加载它们。所以,我们需要想办法让启动类加载UserService。
首先,可以通过@Import(Config.class)
注解让启动类加载Config配置类。
如果Config配置类放到启动类同级目录下就不需要Import注解了,因为会自动扫描加载。
在Config配置类中,通过@ComponentScan("com")
注解,让容器加载整个com包下的bean对象,而不是仅仅加载启动类同级目录下的bean对象。
所以,@Import注解在这里的作用是:引入其他的配置类。
如果我们把所有的配置都写在一个配置类中不便于我们进行管理。所以Spring也支持我们编写多个配置类,只要使用@Import注解引入其他配置类即可。作用相当于
<import>
标签。
@Import注解还有其他两个作用,在分析源码的过程中就可以看到了:
- 引入ImportSelector
- 引入ImportBeanDefinitionRegistrar
1.4 总结
@Import注解具有以下作用,下面将分为三部分介绍:
- 引入其他的配置类
- 引入ImportSelector
- 引入ImportBeanDefinitionRegistrar
二、引入配置类过程探究
2.1 配置类的解析
前面在探究默认组件扫描的时候发现配置类的解析主要在类ConfigurationClassParser
的doProcessConfigurationClass
方法中完成。
通过注释我们可以看出它可以解析@PropertySource 、@ComponentScan 、@Import 、@ImportResource 、@Bean等注解。
// Process any @PropertySource annotations
...
// Process any @ComponentScan annotations
...
// Process any @Import annotations
...
// Process any @ImportResource annotations
...
// Process individual @Bean methods
...
解析@Import是这行代码:
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
那么下面我们就来看看这个方法。
2.2 getImports(sourceClass)
🔶 processImports()
方法
通过断点调试执行到这条代码。
第一次运行到此,可以看到sourceCLass是我们自定义的HelloController。这个HelloController里面并没有我们想要的Import注解。
第二次运行到此,可以 看到sourceClass是我们的启动类,里面有Import注解。
processImports()
方法传入了很多变量,其中有一个getImports(sourceClass)
方法,意思大概是获取Import注解的字节码,所以在之前应先看看getImports(sourceClass)
。
🔶 getImports(sourceClass)
方法
imports
:存放导入进来的SourceClass
visited
:记录已经处理过的SourceClass
🔶 collectImports()
方法
收集所有带有@Import注解的类。
visited.add(sourceClass)
:第一次添加一定会成功,返回true;如果重复添加返回false。递归调用时防止重复调用、重复添加。
sourceClass.getAnnotations()
:获取类上面的所有注解,返回注解集合。
可以看到这里获取了两个注解@Import、@SpringBootApplication。@SpringBootApplication这个注解会递归调用,因为要看看这个注解上面是否包含@Import注解。
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
这段代码获取了配置类的字节码对象,把它放到imports集合里面。
所有使用了@Import注解的类都会添加到这个集合里面。
🔷 所以,这个getImports
方法最后就获取到了所有带有@Import注解的字节码对象。
2.3 processImports()
先经过判断集合是否为空,校验等操作。
开始遍历集合:
前两个元素不看,只看我们自定义的Config。
candidate.isAssignable(xxx.class)
:判断是不是xxx的子类或实现类
Config既不是ImportSelector实现类,也不是ImportBeanDefinitionRegistrar的实现类,所以它会运行到这里:
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
后面将要介绍如何处理配置类,在【3.2.4】中介绍。
2.4 结论
Configuration的doProcessConfigurationClass方法在处理启动类的时候会递归扫描其上所有的@Import注解,获取其中的value属性值添加到集合中。
然后获取遍历这个集合判断他们是ImportSelector的子类还是ImportBeanDefinitionRegistrar的子类还是加了@Configuration的配置类。如果前面两种情况都不满足就说明它是配置类,会继续调用processConfigurationClass方法来解析这个导入进来的配置类。
ImportSelector、ImportBeanDefinitionRegistrar两种情况在后面介绍。
三、ImportSelector探究
当有很多配置类的时,如果使用@Import注解来导入配置类是十分麻烦的。这时可以使用ImportSelector,它可以根据字符串数组(数组元素为类的全类名)来批量的加载指定的bean。
3.1 修改项目
项目结构如下:
🔶 com/test/importselector/MySelector.java
创建MySelector类实现ImportSelector 接口。
通过selectImports()
方法加载配置文件,读取需要加载的全类名,并封装为字符串数组返回。
通过getExclusionFilter()
方法排除一些不要加载的类(它是default方法,非必要可以不实现)。Predicate<T>
是函数式接口,可以通过lambda表达式实现boolean test(T t)
方法,输入s字符串并判断条件返回true或false。
这里的条件是:如果字符串内包含Admin则返回true,也就是说传入com.test.service.AdminService
全类名时返回true,那么将不会加载AdminService。
public class MySelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 读取配置文件中的数据
ResourceBundle rb = ResourceBundle.getBundle("import");
String classNames = rb.getString("className");
// 转换成一个字符串数组返回
return classNames.split(",");
}
/**
* 排除过滤器
* selectImports方法返回的字符串数组中的字符串会传递到test方法里面
* 如果输入的字符串后返回true,那么就不会加载。
* 输入的字符串后返回false,才会加载。
*/
@Override
public Predicate<String> getExclusionFilter() {
// 使用lambda表达式,如果包含Admin字符串则返回true,那么就不会加载
return s -> s.contains("Admin");
}
}
🔶 com/test/service/xxxService.java
@Component
public class AdminService {
}
@Component
public class GroupService {
}
@Component
public class UserService {
}
🔶 src/main/resources/import.properties
配置文件,在MySelector 加载这个配置文件。
className=com.test.service.AdminService,\
com.test.service.GroupService
\
表示换行
🔶 com/springboot/SpringbootApplication.java
@Import(MySelector.class)
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootApplication.class, args);
System.out.println(run.getBean(GroupService.class));
System.out.println(run.getBean(AdminService.class));
}
}
3.2 探究源码
3.2.1 回顾配置类解析
前面已经介绍过了,配置类的解析主要在类ConfigurationClassParser
的doProcessConfigurationClass
方法中完成。
解析@Import
是这行代码:
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
getImports
方法获取到了所有带有@Import注解的字节码对象。
在processImports()
处理过程中有个判断,判断Config是ImportSelector
实现类,还是ImportBeanDefinitionRegistrar
的实现类,如果都不是的话则是普通的配置类。
3.2.2 processImports()
在找自定义的ImportSelector时候,先看到了Springboot本身包含的ImportSelector:
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector
不过暂时先不看它,跳过这个后,看到了我们自定义的
com.test.importselector.MySelector
下面来看看如果是ImportSelector的实现类,具体会做些什么
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
// 加载类获得字节码对象
Class<?> candidateClass = candidate.loadClass();
// 获得ImportSelector对象,就是MySelecor的对象
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
// 过滤器,我们在MySelector里面实现了getExclusionFilter方法
Predicate<String> selectorFilter = selector.getExclusionFilter();
// 如果我们没实现getExclusionFilter方法,这里就是null
if (selectorFilter != null) {
// 如果实现了getExclusionFilter方法,就会把实现的判断条件加进去
exclusionFilter = exclusionFilter.or(selectorFilter);
}
// 判断是不是DeferredImportSelector实现类,这里是false
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
// 不是DeferredImportSelector的实现类,只剩下ImportSelector的实现类
else {
// 获取全类名字符串数组,其中包含com.test.service.AdminService和com.test.service.GroupService
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
// 根据importClassNames的全类名和exclusionFilter过滤条件加载对象,封装为SourceClass对象存储在集合当中
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
// 递归调用processImports方法,因为有可能被加载的类中还有注解
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
下面分别看看asSourceClasses()
方法和processImports()
方法。
3.2.3 asSourceClasses()
下面再看看asSourceClasses()
方法
private Collection<SourceClass> asSourceClasses(String[] classNames, Predicate<String> filter) throws IOException {
// classNames 为字符串数组包含com.test.service.AdminService和com.test.service.GroupService两个字符串
// filter为我们自定义getExclusionFilter()过滤器,当含有Admin字符串时返回true
List<SourceClass> annotatedClasses = new ArrayList<>(classNames.length);
// 遍历字符串数组
for (String className : classNames) {
// 通过asSourceClass()方法处理,并添加到annotatedClasses集合中,这个集合中存储SourceClass对象。
annotatedClasses.add(asSourceClass(className, filter));
}
return annotatedClasses;
}
下面再看看asSourceClass()
方法
// 该方法一定会返回一个SourceClass 对象
SourceClass asSourceClass(@Nullable String className, Predicate<String> filter) throws IOException {
// 如果className为null或者filter条件成立(这里的条件就是MySelector类中getExclusionFilter方法中我们自定义的方法)
// 如果成立就会返回this.objectSourceClass,就是一个普通的Object对象,因为该方法需要一个返回值,所以不能返回null。
if (className == null || filter.test(className)) {
return this.objectSourceClass;
}
if (className.startsWith("java")) {
// Never use ASM for core java types
try {
return new SourceClass(ClassUtils.forName(className, this.resourceLoader.getClassLoader()));
}
catch (ClassNotFoundException ex) {
throw new NestedIOException("Failed to load class [" + className + "]", ex);
}
}
// 如果条件不成立,就把它封装成SourceClass对象并返回
return new SourceClass(this.metadataReaderFactory.getMetadataReader(className));
}
我们可以看到importClassNames字符串数组中包含:
我们可以看到importSourceClasses集合中包含:
其中AdminService被过滤掉了,过滤后只能返回Object的SourceClass对象;而GroupService的SourceClass对象正常加载。
最后再将importSourceClasses传入processImports()方法中递归调用,此时前两个条件不符合,只能当作@Configuration类来进行处理。
那么下面看看如何处理配置类。
3.2.4 processConfigurationClass()
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
...
// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
// 关键点:添加到配置类集合configurationClasses当中
// 后面将要对这个配置类集合进行处理
this.configurationClasses.put(configClass, configClass);
}
继续往下看如何处理configurationClasses
。
3.2.5 processConfigBeanDefinitions()
在ConfigurationClassPostProcessor类的processConfigBeanDefinitions()
方法中。
可以看到这个哈希表中存储了很多配置类
configClasses是将configurationClasses转换成了LinkedHashSet。
后面通过loadBeanDefinitions方法加载成BeanDefinition对象。可以参考BeanDefinition的创建过程
this.reader.loadBeanDefinitions(configClasses);
后面可以通过BeanDefinition创建Bean对象。可以参考Bean对象的创建
3.3 总结
Configuration的doProcessConfigurationClass方法在处理启动类的时候会递归扫描其上所有的@Import注解,获取其中的value属性值添加到集合中。
然后获取遍历这个集合判断他们是ImportSelector的子类还是ImportBeanDefinitionRegistrar的子类还是加了@Configuration的配置类。
如果是ImportSelector则会通过反射创建其对象,调用方法获取字符串数组,封装成SourceClass对象。然后递归调用这些处理import的方法,对这些期望导入的类做同样的处理。
如果导入的不是ImportSelector或者ImportBeanDefinitionRegistrar,会被当成配置类调用processConfigurationClass进行处理。这个方法会把他们添加到一个配置类的哈希表中(configurationClasses)。后面会把配置类集合加载成BeanDefinition对象。
四、ImportBeanDefinitionRegistrar探究
应用场景:如果要实现动态Bean的装载可以使用ImportBeanDefinitionRegistrar。尤其是想要装载动态代理对象的时候。例如Mybatis的启动器就是使用了它实现了Mapper接口的代理对象装载的。
4.1 低级用法
通过Registrar往容器中注入Dog对象。
创建beanDefinition并设置BeanClassName(“com.test.registrar.Dog”),最后注册的bean工厂registry中。
public class SimpleRegistrar implements ImportBeanDefinitionRegistrar {
/**
* 注册beanDefinition
* spring容器可以通过beanDefinition创建bean对象
* @param importingClassMetadata 加了@Import(SimpleRegistrar.class)注解的类的一些信息,如类名、字节码对象
* @param registry bean工厂,beanDefinition就是要注册到bean工厂
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 创建一个Dog对应的BeanDefinition对象
// 创建BeanDefinition实现类对象
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClassName("com.test.registrar.Dog");
// 把BeanDefinition对象进行注册
registry.registerBeanDefinition("dog", beanDefinition);
}
}
启动类,需要加上注解@Import(SimpleRegistrar.class)
@Import(SimpleRegistrar.class)
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootApplication.class, args);
}
}
其实上面这种注册Bean的方法等价于用配置文件注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.test.registrar.Dog" id="dog">
</bean>
</beans>
这只是简单的是练习,当有很多Bean对象的时候并不适用。
4.2 中级用法
想要去识别加了指定注解的类,并把这些类的BeanDefinition对象注册到容器中,这时候就可以使用ClasPathBeanDefinitionScanner。
项目目录如下:
🔶 自定义注解
参考@Component注解的写法,后面我们将自动加载带有这个注解的类。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
}
🔶 ComponentRegistrar
自定装载com.test.registrar包下面带有com.test.registrar.MyComponent注解的类。
public class MyComponentRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 创建Scanner对象
// 使用默认过滤器:true,类上面加上了默认注解才能转换成beanDefinition
// 这里不使用默认过滤器,我们要自定义过滤器
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
// 添加过滤器,在scanner.scan扫描后会把这个类的字节码对象传入到这个过滤器的match方法进行判断
scanner.addIncludeFilter(new TypeFilter() {
/**
* @param metadataReader 这个类的相关信息
* @param metadataReaderFactory
* @return false 被过滤掉 true 不会被过滤
* @throws IOException
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
// 如果这个类加了@MyComponent注解就添加到容器中
return metadataReader.getAnnotationMetadata().hasAnnotation("com.test.registrar.MyComponent");
}
});
// 进行扫描
scanner.scan("com.test.registrar");
// 测试有没有注册BeanDefinition
}
}
🔶 Dog
带有@MyComponent注解
@MyComponent
public class Dog {
}
🔶 启动类
要通过Import注解导入MyComponentRegistrar
@Import(MyComponentRegistrar.class)
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootApplication.class, args);
}
}
4.3 高级用法
FactoryBean:一些对象的创建过程比较复杂,可以使用FactoryBean来实现对象的创建。可以让Spring容器需要创建该对象的时候调用factoryBean来实现创建。尤其是一些动态代理对象的创建。
可以参考FactoryBean创建对象。
🔶 案例目标:
实现类似Mybatis的效果,定义接口,接口上使用指定注解进行标识。然后能生成对应的动态代理对象装载到容器中。
🔶 自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMapper {
}
🔶 给对应接口加上注解
@MyMapper
public interface MyUserMapper {
void select();
}
🔶 创建MyMapperScanner继承ClassPathBeanDefinitionScanner
需要重写isCandidateComponent方法,保证接口能够被加载。因为ClassPathBeanDefinitionScanner的isCandidateComponent方法不会加载接口。
public class MyMapperScanner extends ClassPathBeanDefinitionScanner {
public MyMapperScanner(BeanDefinitionRegistry registry) {
super(registry);
}
public MyMapperScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
super(registry, useDefaultFilters);
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
// 是接口的话,返回true
return metadata.isInterface();
}
}
🔶 创建MapperFactoryBean 实现FactoryBean 接口
通过动态代理创建接口的实现类对象并返回代理对象。
public class MapperFactoryBean implements FactoryBean {
/**
* 用于保存接口的全类名
*/
private String className;
public MapperFactoryBean(String className) {
this.className = className;
}
@Override
public Object getObject() throws Exception {
Class<?> interfaceClass = Class.forName(className);
// 生成动态代理对象返回
Object proxyInstance = Proxy.newProxyInstance(MapperFactoryBean.class.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// 获取当前方法名
System.out.println(method.getName());
if ("select".equals((method.getName()))) {
System.out.println(className + "动态代理对象的select方法被执行了!!");
}
return null;
}
});
return proxyInstance;
}
@Override
public Class<?> getObjectType() {
try {
Class<?> interfaceCLass = Class.forName(className);
return interfaceCLass;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
🔶 创建MyMapperRegistrar实现ImportBeanDefinitionRegistrar接口
public class MyMapperRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 创建扫描器
// 使用默认过滤器:true,类上面加上了默认注解才能转换成beanDefinition
MyMapperScanner scanner = new MyMapperScanner(registry, false);
// 设置一个Include过滤器,判断是否有MyMapper注解
scanner.addIncludeFilter(new TypeFilter() {
/**
* @param metadataReader 这个类的相关信息
* @param metadataReaderFactory
* @return false 被过滤掉,true 不会被过滤
* @throws IOException
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return metadataReader.getAnnotationMetadata().hasAnnotation(MyMapper.class.getName());
}
});
// 进行扫描
Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents("com.test.registrar");
// 扫描到BeanDefinition后进行转换 使用MapperFactoryBean的BeanDefinition进行注册
for (BeanDefinition beanDefinition : beanDefinitions) {
AbstractBeanDefinition factoryBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class)
.addConstructorArgValue(beanDefinition.getBeanClassName()).getBeanDefinition();
registry.registerBeanDefinition(beanDefinition.getBeanClassName(), factoryBeanDefinition);
}
}
}