SpringBoot的自动装配原理解析(第一章)之 通过spring.factories文件跨模块实例化

1,023 阅读5分钟

一则面试经历

 多年前,楼主之前在面试时,架构师问了一个问题,当时有点没想明白,这里分享下:
 如何在Spring中,引用外部容器的Bean对象(好像是这么问的)
 或者说Spring如何来管理外部容器的Bean
 
 举个例子: 
 假设你的工程是SpringBoot启动,启动类包名是 com.springboot.test
 所以你的Spring容器里的管理的Bean默认是 com.springboot.test 这个包扫描下的
 
 假设你引用了第三方jar包,这个是外部容器,它的命名例如是 demo.test,不是在com.springboot.test 这个包下的,也就是不会被扫描到的
 但是我们在使用第三方jar里面的方法时,是直接拿来用的,使用姿势如下:
 @Autowired
 private TestUtil testUtil; 
 testUtil.methodOne()
 
 这里就有一个问题,既然我第三方包名是demo.test,不是在com.springboot.test下的,那么三方包下面的TestUtil类也不会被Spring容器扫描管理,所以我testUtil肯定是空指针,会失败的
 那么这个问题是怎么解决的??
 

从面试经历谈SpringBoot自动装配

何为自动装配
 假设我们一个项目要连接Mysql,使用Mybatis,使用Redis等
 如果纯手动自己写代码,要搞很多花里胡哨的东西,配置数据库连接啊,连接池,Redis方法封装啊各自 
 但是我们真正在开发过程中,就直接导入了一个 ***stater**包完事,里面封装了各种方式
 例如: springboot-redis-starter
      springboot-mybatis-starter
      springboot-mybatis-starter

 然后直接用里面的方法完事:
 例如想redis存储数据,使用姿势如下
 @Autowired
 private RedisUtil redisUtil
 redisUtil.set(key,value)
 
 也就是我们通过注解或者一些简单的配置就能在SpringBoot的帮助下实现某块功能,这就是自动装配,就像我引入了springboot-redis-starter,配置下redis连接信息,整点注解,直接用里面的方法即可
 
再谈面试经历
上面说了自动装配过程中,我们引入了一些springboot-redis-starter 这种包,里面的RedisUtil我们是毫不客气拿来即用, 
再结合上述面试经历看
springboot-redis-starter包名是 *srpingboot.redis**,那它里面的RedisUtil
又是怎么被我的Spring容器管理的?为什么我拿来即用,没有空指针?

专业点讲即是:文件跨模块实例化

META-INF/spring.factories庐山真面目

 如果你是一个爱看源码的同学,你应该对这个文件有一定印象
 它一般会出现在各种**springboot-*-starter* 源码包中,或者公司架构师封装的开箱即用工具包中
 目录src/main/resources/META-INF/**下

wecom-temp-33d2152f4721dc17db25980d63e225c6.png

wecom-temp-504facc973a0d25f044aa1dd693c9a06.png

企业微信截图_3b1473cc-1af8-40c7-aba6-ab7075893fa8.png

也简单总结下基本特点
spring.factories 的文件内容就是接口对应其实现类,实现类可以有多个**
文件内容必须是kv形式,即properties类型**
一个接口可以有多个实现类,注意格式

META-INF/spring.factories原理解析

 我们自身搭建一个SpringBoot工程,因为一个 **springboot-starter**,然后Debug看下源码流程
 
 关于SpringBoot源码启动流程这里不多逼逼了,这里直接上干货,进入主流程
 Debug顺序:
 启动类@SpringBootApplication-->    

wecom-temp-49c6fb1fb1504e871d8aba5b2cc8ec5a.png

 复合注解@EnableAutoConfiguration-->  

wecom-temp-4ded60f2ef70fd7c735a960cf5a67e4b.png

@Import(EnableAutoConfigurationImportSelector.class)-->

wecom-temp-df8201877d5e8467c263a333efb11373.png

AutoConfigurationImportSelector.selectImports()-->

wecom-temp-60383442cc8e978ce7e0e0968b7d8596.png

企业微信截图_6932e1ef-f187-4e08-9d37-1c490279eb15.png

 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);-->
 

wecom-temp-d06da1e751158e313fabbf98b757ae11.png wecom-temp-26c24b85164cb5a971ce5a1be35c5934.png

 !!! 风暴已经出现!!! 源码看到了这里马上就出现一个重要的类了SpringFactoriesLoader
  以及其对应方法,我们继续深入看看这个类,和这个方法做什么了
  SpringFactoriesLoader.loadFactoryNames(
  getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());

wecom-temp-1ccc25cb9bcfa93da639a0405285c963.png

// spring.factories文件的扫描位置
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
   String factoryClassName = factoryClass.getName();
   try {
      // 通过类加载器,获取spring.factories文件
      Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
      List<String> result = new ArrayList<String>();
      // 开始对spring.factories文件读了
      while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         //获取里面的配置信息
         Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
         String factoryClassNames = properties.getProperty(factoryClassName);
         result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
      }
      return result;
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
            "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
   }
}
这里可以看我的Debug数据部分截图
这里可以看出扫描了我**druid** 包下的spring.factories文件,并且获取了org.springframework.boot.autoconfigure.EnableAutoConfiguration 这个key对应的配置值,也就是需要加载的bean  

wecom-temp-2ef1629effbe6dab28a2a2d27871a612.png

或者我直白点:公司架构师写了一个第三方包,com.demo.TestUtil
现在他想把这个TestUtil给我Spring加载进来
就会把TestUtil的信息 放入spring.factories文件里
这样就会被spring里面的SpringFactoriesLoader文件给读取到了,之后******

wecom-temp-34fc96eb6bc67f593159f13dcea73cdb.png

至于后续Spring又是如何加载进去的,那就是另一个点了,有兴趣的同学可以去了解一下Spring bean的几种注入方法,关键词(这里不多逼逼了)
@Import注解,
ImportBeanDefinitionRegistrar
ImportSelector
此处的注入亦是核心入口类AutoConfigurationImportSelector.selectImports()进来的

spring.factories 总结

作用总结
SpringBoot在包扫描时,并不会扫描子模块下的内容,这样就使得我们的其他模块中的Bean无法注入到Spring容器中,SpringBoot为我们提供了spring.factories这个文件,这个文件可以帮助我们将其他模块的Bean注入到我们的Spring容器中

举例:我们引入第三方jar包时,如 druid-starter,redis-starter,将我们需要注入的bean 写入spring.factories文件中,这样SpringBoot会去扫描spring.factories中的信息,将bean进行加载

这就就回答了,上述的问题,即使我们的模块例如是com.test.demo,Spring是不会自动扫描注入第三方模块如demo.util包下的bean的,可以通过此实现跨模块实例化
原理总结
1 Springboot启动时,启动类上的@SpringBootApplication复合注解EnableAutoConfiguration继承了EnableAutoConfigurationImportSelector,AutoConfigurationImportSelector

2 AutoConfigurationImportSelector类中核心方法selectImports()中,会去扫描当前Spring需要加载的Bean对象信息

3 Spring提供了核心类SpringFactoriesLoader,
  这个类的主要作用是去扫描jar下META-INF/spring.factories文件,并加载spring.factories文件中,key为EnableAutoConfiguration对应的value值,即外部三方需要加载的bean类信息

4 SpringFactoriesLoader将扫描到的信息返回给AutoConfigurationImportSelector之后通过Spring进行实例化

写在后面

资质有限,难免有误,还望靓仔不吝赐教