【SpringBoot】原理分析(一):自动装配原理详析

在前两篇我们介绍了 SpringBoot 的基本使用

那现在来思考一个问题,SpringBoot是如何实现自动装配的呢?

自动装配

  • 相对非自动而言,在Spring中当要使用某个外部组件时,必须通过配置或JavaConfig将其手动显示注册
  • 在SpringBoot中,这些外部依赖组件,在引入 相应Stater后就做到了开箱即用,基本0配置(除必要属性)

为了揭开 springboot 的奥秘,我们直接从 Annotation 入手,看看@SpringBootApplication里面,做了什么?

@SpringBootApplication

打开SpringBootApplication这个注解,可以看到它实际上是一个复合注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited  // 可被继承
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { // 默认扫描当前目录(包括类和子包)
      @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

   @AliasFor(annotation = EnableAutoConfiguration.class)
   Class<?>[] exclude() default {};

   @AliasFor(annotation = EnableAutoConfiguration.class)
   String[] excludeName() default {};
   // 根据包路径扫描
   @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
   String[] scanBasePackages() default {};
   // 直接根据class类扫描
   @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
   Class<?>[] scanBasePackageClasses() default {};
}

可以看出核心是以下三个注解:

  1. @SpringBootConfiguration – 标识当前类是配置类
  2. @EnableAutoConfiguration – 通过@Import + ImportSelector 动态导入所有 bean 配置
  3. @ComponetScan – 扫描所有标有 @Controller、@Service、@Componet 等注解的 bean

我们可以直接用这三个注解启动 springboot 应用,因为所有要注册的 bean = @Configuration + @ComponentScan。只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些

在这里插入图片描述

1.@SpringBootConfiguration

点打开 @SpringBootConfiguration 注解,我们可以看到它的关键是 @Configuration

在这里插入图片描述
@Configuration 这个注解大家应该有用过,它是JavaConfig形式的基于Spring IOC容器的配置类使用的一种注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 本质也是一个Bean,需要被扫描发现
public @interface Configuration {
    
   @AliasFor(annotation = Component.class)
   String value() default "";
}

传统的xml形式配置Bean:

<beans>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/test" />
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
<beans>

JavaConfig 配置Bean:

Java5 引入了 Annotations 这个特性,Spring 框架也紧随大流并且推出了基于 Java 代码和 Annotation 元信息的依赖关系绑定描述的方式。也就是JavaConfig。从 spring3 开始 spring 就支持了两种bean的配置方式。

@Configuration // @Configuration(配置类)就相当于一个beans.xml
public class config {
    @Bean // 等价于 <bean>
    // 返回值 = bean的class ; 方法名 = bean的id
    public BasicDataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName();  // 这些设置 = property
        ...
        return dataSource;
    }
}

注:@Configuration本质上也是一个Bean,也需要被扫描

2.@EnableAutoConfiguration

@EnableAutoConfiguration 的主要作用其实就是把所有符合条件的@Configuration配置(实质上是要注册的beans)都加载到当前SpringBoot创建并使用的IOC容器中。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
   // 按类型排除不需要自动装配的类
   Class<?>[] exclude() default {};
   // 按名称排除不需要自动装配的类
   String[] excludeName() default {};
}

在 spring3.1 版本中,提供了一系列的@Enable 开头的注解,Enable主机应该是在JavaConfig框架上更进一 步的完善,使得用户在使用spring相关的框架时,避免配置大量的代码,从而降低使用的难度。比如常见的一些 Enable 注解:

  1. @EnableWebMvc,引入 MVC 框架在 Spring 应用中需要用到的所有 bean
  2. @EnableScheduling,开启计划任务的支持;

找到 EnableAutoConfiguration,我们可以看到每一个涉及到Enable开头的注解,都会带有一个@Import的注解。

2.1 @Import

@Import 就是把多个分散的容器配置合并在一个配置中,类似于 xml 形式下有一个 <import resource/> 形式的注解。

注意,@Import导入是类名.class,可以导入任何类,不要求必须标@Componet交给SpringIOC)

问题一:@Import如何使用?

@Import 它有两种导入 bean 的方式

  • 静态导入:直接指定要注入的 bean
    • 普通Java类
    • @Configuration 配置类
  • 动态导入:运行时再决定注入的 bean,有点点反射的味道
    • 实现 ImportSelector 接口
    • 实现 ImportBeanDefinitionRegistrar 接口

问题二:这里为什么需要 @Import?

每个@Configuration都独立的,其本质上也是一个Bean(需要被扫描),而Import导入就相当于扫描进来了。

比如一个 config 中的 bean 构造时需要依赖另一个 config 中配置的 bean,就要把另一个 config 给 import 进来。而此处EnableAutoConfiguration虽然有@Configuration但其实没有具体的配置,所以需要@Import把具体配置引进来。

这里注意一点,@ComponentScan 也可以把相应配置扫进来,但一般不用它扫配置类。

  1. 普通Bean:如@Controller,@Component 等常用 @ComponentScan
  2. 配置类:常用@Import合并到一个配置中,然后再@CompentScan扫描一处即可
  3. 扩展配置类:如引入的依赖(mybatis,自定义组件等),一般在spring.factories文件中写明要加载的配置类

再回到 EnableAutoConfiguration 这个注解中,我们发现它的 import 是这样

@Import(AutoConfigurationImportSelector.class)

2.2 AutoConfigurationImportSelector

基于 ImportSelector 实现动态装载(根据具体环境决定注册哪些bean),继承关系如下:

在这里插入图片描述

public class AutoConfigurationImportSelector
      implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
      BeanFactoryAware, EnvironmentAware, Ordered {
      
    @Override
    // Spring会把这个方法返回的类的全限定名数组里的所有的类都注入到 IOC 容器中
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 判断当前系统是否禁用了自动装配的功能
		if (!isEnabled(annotationMetadata)) {
            // 如果当前系统禁用了自动装配的功能,则会返回空数组(new String[0]),后续也就无法注入bean了
			return NO_IMPORTS;
		}
        
        // 1.加载配置条件信息
        // META-INF/spring-autoconfigure-metadata.properties文件
		AutoConfigurationMetadata autoConfigurationMetadata = 
									AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
									
        // 2.获取注解的属性及其值(PS:注解指的是@EnableAutoConfiguration注解)
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		
        // 3.根据以上条件获取要装载beanNames,返回一个List<String>(已经被过滤:避免没依赖的bean加载)
        // 在classpath下所有的 META-INF/spring.factories 文件中查找 
        // key = org.springframework.boot.autoconfigure.EnableAutoConfiguration
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        
        // 4.对上一步返回的List中的元素去重、排序
		configurations = removeDuplicates(configurations);
		
        // 5.依据第2步中获取获取@EnableAutoConfiguration注解上的exclude、excludeName属性,排除一些类
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
        
        // 6.返回真正要装载的Beans
		return StringUtils.toStringArray(configurations);
	}
}

本质上来说,其实 EnableAutoConfiguration 会帮助 springboot 应用把所有符合 @Configuration 配置都加载到当前 SpringBoot 创建的IoC容器,而动态注入借助了 Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持。以及用到了 Spring 提供的条件注解 @Conditional,选择性的针对需要加载的 bean 进行条件过滤。

问题一:SpringFactoriesLoader 是什么?

SpringFactoriesLoader这个工具类的使用。它其实和 java 中的 SPI 机制的原理是一样的,不过它比SPI更好的点在于不会一次性加载所有的类,而是根据 key 进行加载(一般是相应注解全类名)。

1).META-INF/spring.factories有很多个,因为Spring启动要有很多Bean启动。

在 SpringBoot 中我们使用每个外部的框架或者组件时,看似只引入了一个 starter,但实际上它最少包含了三个jar包:

  1. 框架/组件本身的 jar 包,比如 mybatis.jar
  2. 框架/组件和Spring集成的 jar 包,因为Spring要对它们的使用再进行封装,比如 mybatis-spring.jar
  3. 框架/组件在Spring中自动装配的 jar 包,比如 mybatis-spring-boot-autoconfigure.jar

所以我们就不难理解为什么类路径下会存在这么多的 spring.factories,因为会有许多的 bean 自动装配好去给用户使用

关于自定义 starter 可以看我的下篇文章 【SpringBoot】手写 starter及自定义配置参数

2). 每个 spring.factories 里面都有一些 key,自动装配的 key 是 org.springframework.boot.autoconfigure.EnableAutoConfiguration

在这里插入图片描述
上面的 mybatis-spring-boot-autoconfiguration 只有自动装配这一个key,再来看一个有多个 key 的 spring.factories


问题二:这些~Configuration到底写的是什么呢?

我们打开上面的 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 来看看

在这里插入图片描述
可以看到,它就是一个提前写好的配置类而已,等到到时候 SpringBoot 启动时,会自动将它装载进去,从而实例化 SqlSessionFactory 和 SqlSessionTemplate。

但是这里还有个问题,类和方法上标的那些 @ConditionOnClass、@ConditionOnBean,@ConditionalOnMissingBean 干嘛的呀?是用来做条件过滤的。

问题三:上面说的条件过滤又是什么?

在分析 AutoConfigurationImportSelector 的源码时,我们看到它会先扫描 spring-autoconfiguration-metadata.properties 文件,最后再扫描spring.factories 对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢?

因为@Configuration其实是依托于其他的框架来加载的, 如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载。比如现在导入上面 RabbitAutoConfiguration 来对 RabbitMQ 的 bean 进行自动装配,但是我项目里根本就没有用到它,连他的依赖都没有,那加载他有什么意义。

所以,通过这种条件过滤可以 有效的减少@configuration类的数量从而降低 SpringBoot的启动时间。

Conditions描述
@ConditionalOnBean在存在某个 bean 的时候
@ConditionalOnMissingBean不存在某个 bean 的时候
@ConditionalOnClass当前 classpath 可以找到某个类型的类时
@ConditionalOnMissingClass当前 classpath 不可以找到某个类型的类 时
@ConditionalOnResource当前 classpath 是否存在某个资源文件
@ConditionalOnProperty当前 jvm 是否包含某个系统属性为某个值
@ConditionalOnWebApplication当前 spring context 是否是 web 应用程序

3.@ComponentScan

ComponentScan 这个注解是大家接触得最多的了,相当 于 xml 配置文件中的<context:component-scan>

<!-- 扫描注解(除了controller外),将 controller 交给 SpringMVC 容器去管理 -->
<context:component-scan base-package="com.xupt.yzh">
	<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

ComponentScan 做的事情就是告诉 Spring 从哪里找到 bean。所以它会扫描指定路径下标识了需要装配的类,自动装配到spring的Ioc容器中。 标识需要装配的类的形式主要是:@Component、 @Repository、@Service、@Controller 这类的注解标识的类。

ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到IoC容器中。

所以,我们必须把启动类放置到代码的根目录下才能保证所有的 bean 都被扫描到!

问题:我好像还听说过个 @MapperScan 它和 @CompentScan 什么关系呢?

  • @ComponentScan
    • 此注解是用来扫描需要加入 IOC 容器的实体类,即是关系项目中类的依赖关系(注意:此注解并不创建类的实例)
    • 默认情况下此注解扫扫描本工程下所有的包,但是如果使用了 basePackages 属性时,此注解就不会使用默认的扫描路径了
  • @MapperScan:
    • 此注解是扫描指定要变成实现类的接口所在包(是属于 mybatis 的注解)
    • 包下面的所有接口,在编译之后 MyBatis 会手动为所有接口创建 BeanDefinition 并注册到 IOC 容器

所以使用 mybatis 时还需在 Spring 单独配置一个 MapperScannerConfigurer 去扫描所有 @Mapper 标识的接口

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
     <!-- 扫描所有dao接口的实现 -->
     <property name="basePackage" value="com.xupt.yzh.dao"></property>
</bean>

但是在 SpringBoot 中就不用再去单独配了,因为在自动装配的 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 中配置了一个静态内部类 AutoConfiguredMapperScannerRegistrar 去扫描 @Mapper 的接口,然后去注册 BeanDefinition

这里再放个参考链接 接口不能被实例化,Mybatis的Mapper/Dao为什么却可以@Autowired注入?

自动装配流程图

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值