在探究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文件如下:

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);
        }
    }
}