实现自动装配的一个小demo及其原理
首先说一下情景,很多时候我们可能会做一些小组件,如果不使用自动装配的话,那么是有点麻烦的,需要手动指定扫描的路径,才可以交给spring去做处理。那么问题来了,有没有一种比较简单的方式可以做到,不用去指定扫描路径,仅仅通过添加maven的依赖就解决了呢。那就是通过spring的自动装配机制。
首先先创建一个工程,作为被依赖的组件,后续再创建一个工程使用这个组件。
组件工程的结构如下图所示

maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
配置类代码
@Configuration
@ComponentScan
public class Configutation {
}
服务的代码
@Service
public class HelloService {
public String sayHello() {
return "auto-config hello";
}
}
随后在resource目录下创建MATE-INF目录,并且放置一个spring.factories文件
spring.factories的内容,其实就是我们自定义需要被自动装配的配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
auto.Configutation
maven install一下
随后我们来创建一个工程,用于测试这个组件是否可以使用
首先添加依赖
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gee</groupId>
<artifactId>sb-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 开启热部署 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 自定义的组件-->
<dependency>
<groupId>com.gee</groupId>
<artifactId>my-auto-configuration</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 指定maven编译的jdk的版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 打包成springboot专用的jar包,指定入口信息等等 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置类
@SpringBootApplication
public class Config {
public static void main(String args[]) {
SpringApplication.run(Config.class, args);
}
}
获取bean的一个工具类,必须位于配置类的包/子包路径下,因为没有指定ComponentScan的包路径,默认就是配置类的包/子包路径下
@Component
public class ApplicationContextUtils implements ApplicationContextAware{
private ApplicationContext context;
public static ApplicationContext APP_CONTEXT;
public <T> T getBean(Class<T> t) {
return context.getBean(t);
}
public String[] getBeanNames() {
return context.getBeanDefinitionNames();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
APP_CONTEXT = applicationContext;
}
public ApplicationContext getContext() {
return context;
}
public void setContext(ApplicationContext context) {
this.context = context;
}
}
在src/test/java下,创建一个包,顺便写一个测试的启动类,以及对应的测试方法,测试一下
@RunWith(SpringRunner.class)
@SpringBootTest(classes={Config.class})
public class ConfigTest {
@Test
public void getProducer() {
System.out.println(ApplicationContextUtils.APP_CONTEXT.getBean(HelloService.class).sayHello());
}
}
输出结果如下
auto-config hello
2020-07-18 11:38:50.332 INFO 4859 --- [ Thread-10] o.s.w.c.s.GenericWebApplicationContext : Closing org.springframework.web.context.support.GenericWebApplicationContext@306e95ec: startup date [Sat Jul 18 11:38:22 CST 2020]; root of context hierarchy
2020-07-18 11:38:50.342 WARN 4859 --- [ Thread-10] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'rocketMQTemplate': java.lang.IllegalStateException: Shutdown in progress
2020-07-18 11:38:50.342 WARN 4859 --- [ Thread-10] o.s.b.f.support.DisposableBeanAdapter : Destroy method 'shutdown' on bean with name 'defaultMQProducer' threw an exception: java.lang.IllegalStateException: Shutdown in progress
说明这种用法是可行的。
那么下面来说说原理。这里就不说的太详细了,下面是之前对自动装配原理的一些分析
https://www.jianshu.com/p/3ae65b2b4087
https://www.jianshu.com/p/f6966f2c7d8d
那么为什么组件中的spring.factories必须得以org.springframework.boot.autoconfigure.EnableAutoConfiguration作为key呢?
答案也是很简单的。spring给自动装配定义了好几种类型,而org.springframework.boot.autoconfigure.EnableAutoConfiguration是其中一种类型。
先来看看流程吧。

从流程我们可以看出,入口还是在容器刷新的时候,通过执行beanDefinitionRegistryPostProcesor,完成自动装配。
那么我们看看执行的关键代码。注释中,会讲解每个步骤的大致作用。
class ConfigurationClassParser {
private void processDeferredImportSelectors() {
//如果成员变量中的导入选择器为null,则说明不需要进行处理
//成员中的导入选择器其实是在处理@import标签时,判断如果是importSelector类型,则会放置到成员变量中。
//@SpringBootApplication->@EnableAutoConfiguration->@Import(AutoConfigurationImportSelector.class)
//所以实际上,这里的导入选择器只会是AutoConfigurationImportSelector
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
if (deferredImports == null) {
return;
}
//先进行排序
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
//这里其实就是组->组对应的导入选择器,grouping封装了group以及List<ImportSelector>
Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
//根据importSelector中的group进行分组
for (DeferredImportSelectorHolder deferredImport : deferredImports) {
//获取该导入选择器的组
Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
//若该分组不存在则进行创建
DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent(
(group != null ? group : deferredImport),
key -> new DeferredImportSelectorGrouping(createGroup(group)));
//将该导入选择器加入到对应的分组中
grouping.add(deferredImport);
configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getConfigurationClass());
}
//遍历执行每个分组,由于这里只有AutoConfiurationImportSelector所以我们只关注这个类即可。
for (DeferredImportSelectorGrouping grouping : groupings.values()) {
//获取需要自动装配的所有类,逐个去处理,那么如何获取则是关键。
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = configurationClasses.get(entry.getMetadata());
try {
//处理自动装配的类,这里其实就是对自动装配的配置类进行处理,与处理@import的方法是一致的,感兴趣可以往里面看看。
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}
}
如何获取自动装配的类,定位到最后,与预期一致AutoConfigurationGroup,AutoConfigurationImportSelector是核心
public class AutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
private static class AutoConfigurationGroup implements DeferredImportSelector.Group,
BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
@Override
//AutoConfigurationGroup对应的importSelector只有AutoConfigurationImportSelector,所以看看AutoConfigurationImportSelector的selectImports方法即可。
public void process(AnnotationMetadata annotationMetadata,
DeferredImportSelector deferredImportSelector) {
String[] imports = deferredImportSelector.selectImports(annotationMetadata);
for (String importClassName : imports) {
this.entries.put(importClassName, annotationMetadata);
}
}
}
AutoConfigurationImportSelector如何找到自动装配的类,看下面方法
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
//获取配置类的方法,获取key=EnableAutoConfiguration的className对应的所有配置类
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
//这个方法其实就帮我们指定了,我们要获取spring.factories内容中key为EnableAutoConfiguration的className的内容
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
SpringFactoriesLoader的工作原理
//这个方法其实就是加载所有的spring.factories,获取所有的文件内容,并且生成一个map,然后根据传进来的 factoryClassName作为key,取factoryClassName对应的values,我们刚刚传进来的factoryClassName不就是org.springframework.boot.autoconfigure.EnableAutoConfiguration吗?这个key也跟我们自定义的spring.factories的key一样,所以key是必须使用spring这里规定的.如果我们自己随意定义一个key,则并不会被spring处理。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
//这个类就是加载meta-inf下的spring.factories的内容,并且生成map
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
result.addAll((String) entry.getKey(), factoryClassNames);
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}