深度解析SpringBoot自动装配原理

自动装配原理

从springboot启动类注解@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 {...}

可以看到它实际上是一个复合注解,上面四个是元注解,下面三个才是重点

@Configuration
@EnableAutoConfiguration
@ComponentScan
我们可以直接用这三个注解也可以启动 Spring Boot 应用,只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些。下面是官网的截图,刚兴趣的小伙伴可自行翻译。

@Configuration
@Configuration 这个注解大家应该都用过,它是 JavaConfig 形式的基于 Spring IOC 容器的配置类使用的一种注解。所以在启动类里面标注了 @Configuration,意味着它其实也是一个 IoC 容器的配置类。

传统意义上的 Spring 应用都是基于 xml 形式来配置 bean 的依赖关系。但是从 Spring3 开始,Spring 就支持了两种 bean 的配置方式,一种是基于 xml 文件方式,另一种就是 JavaConfig,任何一个标注了 @Configuration 的 Java 类定义都是一个 JavaConfig 配置类。而在这个配置类中,任何标注了 @Bean 的方法,它的返回值都会作为 Bean 定义注册到 Spring 的 IoC 容器,方法名默认成为这个 Bean 的 id。然后通过 spring 容器在启动的时候,把 Bean 进行初始化并且,如果 Bean 之间存在依赖关系,则分析这些已经在 IoC 容器中的 Bean 根据依赖关系进行组装。

@ComponentScan
@ComponentScan 这个注解大家也用过,这个很简单,就是扫包,相当于 xml 配置文件中的
context:component-scan 。它的主要作用就是扫描指定路径下的标识了需要装配的类,自 动装配到 Spring 的 IoC 容器中。

标识需要装配的类的形式主要是:@Component、@Repository、@Service、@Controller 这类的注解标识的类。(注:@Repository、@Service、@Controller 的底层还是 @Component)。ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到 IoC 容器中。

@EnableAutoConfiguration
好,主角登场了,@EnableAutoConfiguration 是 Spring Boot 的灵魂,是重中之重。从 Spring3.1 开始,提供了一系列的 @Enable 开头的注解,它是在 JavaConfig 框架上更进一步的完善,使用户在使用 Spring 相关的框架避免配置大量的代码从而降低使用的难度。
比如常见的一些 Enable 注解:@EnableWebMvc、@EnableScheduling、@EnableAsync 等等。每一个涉及到 Enable 开头的注解,都会带有一个 @Import 的注解, @EnableAutoConfiguration 也不例外,我们点进去发现如红框所示。

@Import 注解是什么意思呢?它对应 XML 形式下的 ,就是导入资源,把多个分布在不同容器下的配置合并在一个配置中。@Import 注解可以配置三种不同的 class :

普通 Bean 或者带有 @Configuration 的配置文件
实现 ImportSelector 接口进行动态注入
实现 ImportBeanDefinitionRegistrar 接口进行动态注入
这里导入的是第二种 importSelector,这是一种动态注入 Bean 的技术,我们把 AutoConfigurationImportSelector 点进去,发现它实现了 ImportSelector 接口。

找到实现方法 selectImports ,该方法的作用就是找到相应的 Bean 注入到容器中。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
			.loadMetadata(this.beanClassLoader);
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
			annotationMetadata);
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

复制代码

再从 getAutoConfigurationEntry 方法点进去,这里面做了许多事情,就是把找到的 Bean 进行排除、过滤、去重,我们可以看到 removeDuplicates、remove、filter 等方法。

那具体这些 Bean 从哪里找呢,我们将 getCandidateConfigurations 方法点进去,发现了一个惊天秘密,那就是在这有一个 META-INF/spring.factories 文件。

当然这是一个报错信息,我们不敢断定就是这里,没关系,我们把 SpringFactoriesLoader.loadFactoryNames 点进去,发现这里有个变量 FACTORIES_RESOURCE_LOCATION。

而这个变量的值还是 META-INF/spring.factories。

看到这里我们很激动,于是我毫不犹豫的在项目中搜索这个文件,原来 SpringFactoriesLoader 的作用就是从 classpath/META-INF/spring.factories 文件中,根据 key 来加载对应的类到 Spring IoC 容器中。

看到这里小伙伴就明白了,就是把这么多 Configuration 下的 Bean 加载到容器里嘛,

但是 But

,怎么还有 RabbitMQ、Elasticsearch 这些我都用不到,怎么也给加到容器里来了,那多浪费空间和内存啊?小伙伴莫慌,于是我又带着好奇心理打开了 RabbitMQ 的配置类。

看到这里终于舒了口气,小伙们有没有发现这里多了一些 Conditional 的注解,其实这些就是条件注解,Spring Boot 也不傻,它会发现如果当前的 classpath 环境下没有相关联的依赖,则意味着这些类没必要进行加载。所以,通过这种条件过滤可以有效的减少 @configuration 类的数量从而降低 Spring Boot 的启动时间。

Conditions 描述
@ConditionalOnBean 在存在某个 bean 的时候
@ConditionalOnMissingBean 不存在某个 bean 的时候
@ConditionalOnClass 当前 classpath 可以找到某个类型的类时
@ConditionalOnMissingClass 当前 classpath 不可以找到某个类型的类时
@ConditionalOnResource 当前 classpath 是否存在某个资源文件
@ConditionalOnProperty 当前 jvm 是否包含某个系统属性为某个值
@ConditionalOnWebApplication 当前 spring context 是否是 web 应用程序
好,有了上面这么多预备知识后,就可以开始手写一个我们自己的 starter 了。

手写 starter
starter 工程的命名
starter 是一个开箱即用的组件,减少不必要的重复代码,重复配置。例如,如果要使用 Spring 和 JPA 进行数据库访问,在项目中引用 spring-boot-starter-data-jpa 即可。

分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!

Spring 官方定义的 starter 通常命名遵循的格式为 spring-boot-starter-{name},例如 spring-boot-starter-web。非官方 starter 命名应遵循 {name}-spring-boot-starter 的格式,例如,dubbo-spring-boot-starter。

需求
写一个序列化的插件,并且可以自由的选择 fastjson 还是 gson,如果没选的情况下默认选择 fastjson。

步骤
1、 创建一个 Spring Boot 项目,这里项目名字叫 jackformat-spring-boot-starter

2、引入依赖

  
      org.springframework.boot
      spring-boot-autoconfigure
  


  
      org.springframework.boot
      spring-boot-configuration-processor
      true
  
  
  
      com.alibaba
      fastjson
      1.2.56
  

  
      com.google.code.gson
      gson
      2.2.4
  

复制代码

3、先定义一个格式化的接口,分别写两个实现类

public interface FormatProcessor {

/**
 * 定义一个格式化的方法
 *
 * @param obj
 * @param 
 * @return
 */
 String format(T obj);

}
复制代码

4、写一个配置类,这里用了条件注解,如果 fastjson 和 gson 类存在的情况下才加载对应的实现类,因为在 pom 文件里都引用了,所以这里都会被装载。注意这里红框标的 @Primary,对同一个接口,有几种不同的实现类时,@Autowired 是按类型注入的,不知道要选哪一个,按照第二点需求,用户在没选的情况下默认选择 fastjson,所以这里给 fastjson 的实现上打上 @Primary。

5、配置类,用来读取用户的选择,作用和 @Value 一样,只是用了 jackxu.format 的前缀,这样更方便。

6、序列化实现类,这个就是提供给用户用来序列化用的,看名字 Template 大家也能知道,比如我们常用的 RedisTemplate、JdbcTemplate,构造函数的时候直接传入具体的实现。

7、好,现在就是最关键的主类了,我们从上往下看,@Import 之前说过了,导入配置类,就是将该配置类中的 Bean 注入到容器,@EnableConfigurationProperties 这是在将属性类激活,注入到容器中,也可以用 @Bean 的方式,@Configuration 说明这是一个配置类。接下来将 FormatTemplate 注入到容器中,我们看到首先是去属性类中去读属性,如果是 fastjson 就返回 fastjson 的实现,如果是 gson 就返回 gson 的实现,如果没读取到,就用前面设置的 @Primary 的默认实现。

8、最后一步最关键的就是设置,在 resources 文件夹下创建 META-INF/spring.factories 文件,通过上面的知识,Spring Boot 在启动的时候就是读取该文件下的配置类,从而将 Bean 加载到容器中。

测试
1、将自己的 starter 项目进行 install 打包

2、测试项目中引用自己的 starter

3、写一个 controller,一个测试类,并把 formatTemplate 注入进来

4、设置我们需要制定的序列化方式,这里选用 fastjson

5、启动 Spring Boot 项目

6、通过浏览器访问,发现这里显示的是 fastjson 方式的序列化,成功了!

7、在测试 gson 的方式,返回了默认 gson 的实现,也成功了!

8、最后测试用户不选择的情况下,默认使用 fastjson,圆满成功!

至此,本个需求已经成功做出来了,我力求在做的过程中将手写一个 starter 所需用到的技术都讲到,串联起来,希望大家喜欢!

后记

你可能感兴趣的:(深度解析SpringBoot自动装配原理)