3分钟让你搞懂SpringBoot自动装配(面试系列)

244 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

即今江海一归客,他日云霄万里人。

什么是SpringBoot?

什么是SpringBoot?

SpringBoot我们可以理解为帮助开发者快速构建SpringFramework以及Spring整个生态体系的应用解决方案,也是SpringFramework“约定优于配置”理念的最佳实践。

SpringBoot起源

要搞清楚这一点,我们需要从Spring Framework说起,那在Spring之前呢,就是EJB容器技术作为主要的软件解决方案。这里我们不去深入讨论Spring Framework的起源以及EJB容器技术,直入主题,我们来聊一聊当前最热门的SpringBoot。

IOC/DI

什么是IOC,什么是DI?

Spring中的IOC和DI是Spring绕不过去的东西,那如何理解IOC和DI呢?

IOC(控制反转):Spring把对象的生命周期托管到Spring容器中,从而实现了对象获取方式的反转,从原来的自主管理对象变成了由Spring容器来管理,在Spring中创建的对象都会加入到Spring容器中进行统一管理。如果我们需要用到对象,可以不通过new的方式来创建,而是直接从Spring容器中取出对象使用,这样做的目的是解耦,可以降低代码之间的耦合度。

DI(依赖注入):简单来说,我们使用Spring容器来管理多个对象时,怎么知道多个对象之间的依赖关系?比如A对象中使用和依赖了B对象,这种依赖关系我们应该如何知道,如何管理?没错!就是通过依赖注入的方式,实现依赖注入的方式有三种,分别是构造方法注入,接口注入,setter注入。不过现在基本上都是基于注解的注入方式,如@Autowired,@Inject,@Resource,形式变了,但本质上是一样的。

Bean装配方式升级

从Spring的更新迭代中,Bean装配方式有什么变化?

简单说,从以前的基于XML方式的配置,到现在基于注解的方式的配置方法,都依赖在Spring3.x之后引入了JavaConfig的能力,这使得我们从之前繁琐的配置中解放出来,更多的精力注重与业务逻辑的实现上面。比如我们可以使用@Configuration来描述Bean与Bean之间的依赖关系,用@Bean来将对象注入容器,使用@ComponentScan来扫描类并装载到Spring容器等。

那看起来是已经很不错了,还有什么问题?

当然有,通过注解可以减少使用xml配置产生的问题,但殊途同归,问题依然存在:

  • 依赖过多:Spring可以整合多数的框架,如JSON,MyBatis等,不同依赖包可能存在版本兼容问题。
  • 配置太多:就单单是整合上述其中一个技术框架,如MyBatis就需要配置很多东西,例如配置数据源,配置事务管理器,配置注解驱动等。
  • 运行和部署麻烦:需要打包,再部署到容器上。

SpringBoot诞生

为了解决上面的痛点,SpringBoot应运而生,其主要作用就是简化Spring应用的开发。而达到这一目的的基石就是“约定优于配置”。

如何理解约定优于配置?

约定优于配置并不是SpringBoot独创的,这是一种软件的设计范式,通俗一点说,一个大家庭,儿子负责读书,爸爸负责挣钱,妈妈负责家务,家里的锅碗瓢盆存放的位置,这也是一种约定优于配置的体现。

那么对于SpringBoot来说,约定优于配置体现在比如

  • Maven目录结构的约定
  • 对于Starter组件完成自动装配
  • SPI路径的约定
  • SpringBoot默认配置文件以及配置文件中属性的约定

以往我们使用SpringMVC来构建一个Web服务需要很多的基础操作,添加很多的Jar包以及依赖,在web.xml中配置控制器等。那SpringBoot就完全帮我们省略掉了这些操作,使得我们可以更专注于业务本身。那SpringBoot是如何实现的呢?

SpringBoot的核心-自动装配

SpringBoot的核心其实不只是自动装配,还有Starter组件,SpringBoot CLI等等,只不过最核心的当属是自动装配了。自动装配是SpringBoot Starter的核心,也是整个SpringBoot的核心,那我们从一个SpringBoot整合Redis的过程来了解SpringBoot是如何实现自动装配的。

首先在pom文件中我们引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在application.properties中配置数据源:

spring.redis.host=localhost
spring.redis.port=6379

在Controller中操作redis:

@RestController
public class HelloController {
    @Autowired
    RedisTemplate<String,String> redisTemplate;
​
    @GetMapping("/hello")
    public String hello(){
        redisTemplate.opsForValue().set("key","value");
        return "hello world";
    }
}

那么问题来了,我们并没有使用xml或者注解的形式来将RedisTemplate对象加入到Spring容器,那我们是如何直接使用它的呢?

这也就是我们前面所说的SpringBoot的自动装配,我们在只引入一个starter依赖的情况下,将该组件下符合条件的Bean自动注入到了SpringBoot的容器中,实现了自动装配。其实我们也可以想到这是在某个约定的情况下,将符合约定条件的Bean自动注入来实现自动装配,这也就是约定优于配置的体现。那么具体是什么原理,怎么自动注入,基于什么约定呢?我们接着往下看。

SpringBoot自动装配的实现原理

首先,我们需要看到SpringBoot的启动类,我们找到@SpringBootApplication这个注解,进入这个注解,我们可以看到一个@EnableAutoConfiguration注解,此注解是自动装配的核心,也就是说SpringBoot的自动装配是通过这个注解来开启的。

image-20220426222008172.png

@EnableAutoConfiguration注解

在说这个注解之前,先跟大家说一下@Enable注解。在文章前面跟大家提到过,如果使用JavaConfig的形式来实现Bean的装载,就要用到@Configuration注解和@Bean注解,那么@Enable注解实际上就是对这两个注解的封装。如果大家关注过这类的注解,应该都会知道,这些注解中都会存在一个@Import注解,目的是解析@Import注解导入的配置类,通过其中的描述来选择注入到SpringIOC容器中。那@EnableAutoConfiguration注解会不会是类似的情况呢?

进入到@EnableAutoConfiguration注解中我们可以看到,除了通过@Import注解导入了一个AutoConfigurationImportSelector类以外,还用了一个@AutoConfigurationPackage注解,这个注解的作用是把使用了该注解的类所在的包以及子包下所有的组件扫描到SpringIOC容器中。

那我们接下来再来聊一聊这个AutoConfigurationImportSelector类,这也是@Enable注解和@EnableAutoConfiguration注解最大的不同,@Enable注解导入的是一个Configuration的配置类,而@EnableAutoConfiguration则有所不同。

我们先不去管这个类到底是个什么东西,可以确定的是,它一定实现了配置类的导入,至于与@Enable类的区别,我们也可以看看。

image-20220426223007902.png 点进去这个类,我们可以看到这个类实现了一个ImportSelector接口,它有一个selectImports抽象方法,这个方法返回一个String数组,在这个数组中,可以指定需要装配到IOC容器中的类。

当我们使用@Import导入一个ImportSelector的实现类之后,会把selectImports方法返回的Class名称都装载到IOC容器。

image-20220427221801106.png

与@Configuration不同的是,它可以实现批量装配,并且可以通过逻辑处理来选择哪些类需要被装载到IOC容器中。我们通过一个自己创建的Selector来理解一下:

首先我们创建两个类:

public class Class1 {
}
public class Class2 {
}

再创建一个ImportSelector的实现类,在实现类中把定义的上面两个类加入到String数组中,也就是我们需要把它们装载到IOC容器中。

public class FortuneImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{Class1.class.getName(),Class2.class.getName()};
    }
}

接下来我们再自定义一个注解来模拟@EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({FortuneImportSelector.class})
public @interface EnableFortuneConfiguration {
}

再创建一个启动类,在类上使用我们上述的注解

@SpringBootApplication
@EnableFortuneConfiguration
public class FortuneAutoImportApplication {
    public static void main(String... args){
        ConfigurableApplicationContext run = SpringApplication.run(FortuneAutoImportApplication.class, args);
        Class1 bean = run.getBean(Class1.class);
        Class2 bean1 = run.getBean(Class2.class);
    }
}

这样配置的好处在于可以实现批量的Bean的装配,同样我们可以把我们上面的Class1和Class2改成两个@Configuration配置类,因为配置类代表的是某一个技术组件中批量的Bean的声明,所以在自动装配的这个过程中,我们只需要扫描到配置类即可。

自动装配原理分析

首先我们根据selectImport方法来追踪,看看都干了些什么:

  • 通过getAutoConfigurationEntry获取到了需要加载的Bean
  • 将Bean转换成字符串数组返回

image-20220427225519354.png

那么是怎么获取需要装载的Bean的呢?

  • getAttributes获取@EnableAutoConfiguration注解中的属性exclude,excludeName等。
  • getCandidateConfigurations获取所有自动装配的配置类
  • 然后接下来就是去重以及去掉exclude标志的一些不需要装配的配置类
  • 然后一个广播事件
  • 最后返回

image-20220427225613103.png

我们来看其中核心的方法,getCandidateConfigurations是怎么获取所有自动装配的配置类的

  • 它在里面用到了SpringFactoriesLoader,它是Spring内部约定俗成的一种加载方式,类似于Java中的SPI,从下面的message中我们也可以看到,它会去扫描META-INF/下面的spring.factories文件,然后这里所使用的的loadFactoryNames方法会根据spring.factories文件中的key值来获取当前key值对应的value,这里的key值对应的EnableAutoConfiguration。
  • 下面我也给大家看看每个组件的spring.factories文件,文件是以键值对的形式,也可以是一个key对应多个value,所以会返回一个value数组。

image-20220427225816442.png

image-20220427231134992.png 到这里其实自动装配的原理基本上已经分析完了,我们来总结一下,免得太过于混乱:

  • 通过SpringBootApplication,看到@EnableAutoConfiguration注解的声明
  • 在@EnableAutoConfiguration注解中,使用Import(AutoConfigurationSelector.class)来实现配置类导入
  • AutoConfigurationSelector实现了ImportSelector接口,重写了selectImport方法,实现了有选择性的批量装配的功能
  • 通过Spring提供的SpringFactoriesLoader机制,扫描META_INF/spring.properties文件,读取需要实现自动装配的配置类
  • 通过条件筛选的方式,把不符合条件的移除,实现自动装配

后记

好了,写到这里其实已经消耗了我许多的脑细胞了,大家可以跟着文章的思路自己看一下原本,因为JDK版本的不同,可能源码会有些许不同,但殊途同归。

其实我们甚至可以照着上述步骤自己手写一个简易的starter来实现自动装配,下次有机会给大家呈现,还有就是上面文章没有提到@Conditional注解,我们也可以通过@Conditional来实现条件装配,作用也就是为自动装配实现一定的条件约束,有兴趣大家可以看一看。

最后我想说,阅读源码是一个探索的过程,就像是你刚进入公司接受别人的代码一样,有的人的代码优雅,有的人的代码让人恶心,而你阅读源码恰好是前者,当你惊叹于作者手法之巧妙的时候,也是你学习并提升的过程。不幸的是,你其实在工作中没有选择的权利,亦优雅亦粗俗的代码,你也得往下梳理。

有的选,为什么不选?

有兴趣的话,希望大家可以关注我的公众号【类似程序员】,会定期与大家分享我的一些想法与学习感悟,谢谢各位!