Spring Boot自动配置原理与实践(一)

前言

  Spring Boot 众所周知是为了简化 Spring 的配置,省去 XML 的复杂化配置(虽然 Spring 官方推荐也使用 Java 配置)采用 Java+Annotation 方式配置。如下几个问题是我刚开始接触 Spring Boot 的时候经常遇到的一些疑问,现在总结出来希望能帮助到更多的人理解 Spring Boot,当然这只是个人的理解,稍微显得肤浅但易懂!当时我明白了以下几个问题后,觉得 Spring Boot 也不过如此,没有啥花里胡哨的,希望能帮到大家!

  本博文主要由两个部分组成:第一篇通过源码等形式介绍自动配置的原理与组成部分,第二篇通过实现自定义的 starter 实现自动配置加深对自动配置的理解。

  注:创作不易啊,虽然写只写了 5 个小时左右,但是整个准备过程很早就开始了,当然这样的记录方式主要是督促自己对 Spring Boot 要深入理解,顺带希望能帮到更多人

  问题一:那么 Spring Boot 是靠什么样的手段简化配置呢?

  从广义上讲,从软件设计范式来说。Spring Boot 提供一种“约定优于配置”来简化配置;减少开发人员去开发没必要的配置模块时间,采用约定,并且对不符合约定的部分采用灵活配置满足即可!

  问题二:那么什么叫约定优于配置呢?

  比如:我们知道 Spring Boot 都是内嵌 Tomcat、Jetty 等服务器的,其中默认的 Tomcat 服务器,相应的监听端口也默认绑定,这些默认即为约定大部分情况下我们都会使用默认默认的 Spring Boot 自带的内嵌的默认自动配置约定,不会大量去自定义配置,即这就是我理解的约定优于配置。那么问题三来了。当我们觉得不符合我们实际生产环境的时候才会去改变默认配置,那么需要很麻烦的手段吗?

  问题三:更改默认的端口或者服务器(即上述提到的约定)需要很麻烦的代码编写吗?甚至需要更改源码吗?

  答案肯定是否定的,只需要更改 application.yml/properties(application-*.yml/properties)配置文件即可。当然也可以从源头上开始重新创建自己需要的模块(准确来说可以是 starter),需要的注册的配置类 Bean。从而达到灵活的自动配置目的,这就是本篇要介绍的 Spring Boot 的自动配置。

  明白以上三个问题后,其实就能理解为什么 Spring Boot 相对于 Spring 更加灵活,方便,简单的原因了 ========>无非就是大量使用默认自动配置 + 灵活的自动配置

 

 


 

一、Spring Boot 自动配置的原理介绍

 

  主要从 spring boot 是如何启动后自动检测自动配置与 spring boot 如何灵活处理自动配置两个方面讲解,前者主要分析源码,从简单的注解入手到最后的读取 spring.facoties 文件。后者主要举例 RedisAutoConfiguration 自动配置来说明 spring boot 对于相关的配置类 bean 的通过注解的灵活处理。

1、Spring Boot 是如何发现自动配置的?

都知道 Spring Boot 程序入口处(xxxApplication.java)都会有 @SpringBootApplication 的注解。这个注解表明该程序是主程序入口,也是 SpringBoot 应用

@SpringBootApplication
public class DemoApplication {
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> main(String[] args) {
    SpringApplication.run(DemoApplication.</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">, args);
}

}

查看 @SpringBootApplication 注解,不难发现是一个组合注解,包括了 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan 。换句话说这三个注解可以替代 @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}
)})

其中注解  @EnableAutoConfiguration  就是实现 SpringBoot 自动配置的关键所在,值得注意的是 @EnableXXXX 注解并不是 SpringBoot 特有,在 Spring 3.x 中就已经引出,比如:@EnableWebMvc 用来启用 Spring MVC 而不需要 XML 配置,@EnableTransactionManagement 注释用来声明事务管理而不需要 XML 配置。如此可见,@EnableXXXX 是用来开启某一个特定的功能,从而使用 Java 来配置,省去 XML 配置。在 SpringBoot 中自动配置的开启就是依靠注解  @EnableAutoConfiguration  ,继续查看注解接口 @EnableAutoConfiguration 

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class</span>&lt;?&gt;[] exclude() <span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {};

String[] excludeName() </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> {};

}

其中关键是 @Import({AutoConfigurationImportSelector.class}) ,表示选择所有 SpringBoot 的自动配置。废话不多说,还得继续看该选择器源码,其中最主要的方法是 getAutoConfiguationEntry 方法,该方法的主要功能是:

获得自动配置 ----> 删除重复自动配置 -----> 删除排除掉的自动配置 ----> 删除过滤掉的自动配置 ------> 最终的自动配置

这里我们主要关注如何获取自动配置,即图中红圈部分,即 getCandidateConfigurations 方法,该方法通过 SpringFactoresLoader.loadFactoryNames 方法读取某个 classpath 下的文件获取所有的 configurations

注意到: No auto configuration classes found in META-INF/spring.factories..... ,它应该是从 META-INF/spring.factories 文件中下读取 configurations,我们再往下查看方法 SpringFactoresLoader.loadFactoryNames ,该方法果然是读取 META-INF/spring.factories 文件的,从中获取自动配置键值对。

 

最后,我们找到 META-INF/spring.factories 文件(位于 spring-boot-autoconfigure 下)

查看其内容:

...
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
...

 这时候才恍然大悟,原来如此。简单总结起来就是,当 SpringBoot 应用启动读取自动配置之后,从上到下的相关调用栈(文字简单描述)

  • @EnableAutoConfiguration 开启启动配置功能
  • 通过 @Import 导入 AutoConfigurationImportSelector 选择器发现(注册)自动配置
  • getAutoConfigurationEntry 方法获取并处理所有的自动配置
  • SpringFactoriesLoader.loadFactoryNames 读取 spring.factories 文件并加载自动配置键值对。

 

 2、Spring Boot 是如何灵活处理自动配置的?

 

  从 spring.factories 文件中我们选取 RedisAutoConfiguration 举例说明,至于为什么选这个呢?这是因为之前我有写过关于 Spring Boot 整合 Redis 的相关博文。所以好入手!

首先,还是一样进入 RedisAutoConfiguration(Idea 大法好,点击进入即可查看代码)

源码如下,主要关注以下几个注解:

1)@ConditionalOnClass在当前 classpath 下是否存在指定类,若是则将当前的配置注册到 spring 容器。可见其灵活性。相关的 @ConditionalXXX 注解是 spring 4.x 推出的灵活注册 bean 的注解,称之为“条件注解”。有如下一系列条件注解:

  • @ConditionalOnMissingBean: 当 spring 容器中不存在指定的 bean 时,才会注册该 Bean。
  • @ConditionalOnMissingClass:当不存在指定的类时,才会注册该 bean。
  • @ConditionalOnProperty:根据配置文件中某个属性来决定是否注册该 Bean,相对于其它条件注解较为复杂。主要有以下两种情况应用:

  一种是 @ConditionalOnProperty(prefix = “mysql”, name = “enable”, havingValue = “true”) 表示以为 mysql 为前缀的 mysql.enable 属性如果为空或者不是 true 时不会注册指定 Bean,只有 mysql.enable 属性为 true 时候才会注册该指定 Bean

    一种是 @ConditionalOnProperty(prefix = “mysql”, name = “enable”, havingValue = “true”, matchIfMissing = true)matchIfMissing 表示如果没有以 mysql 为前缀的 enable 属性,则为 true 表示符合条件,可以注册该指定 Bean,默认为 false。

  • @ConditionalOnJava:是否是 Java 应用
  • @ConditionalOnWebApplication:是否是 Web 应用
  • @ConditionalOnExpression:根据表达式判断是否注册 Bean

  ........... 剩下的还有很多,在 autoconfigure.condition 包下...........

    

 

2)@EnableConfigurationProperties让 @ConfigurationProperties 注解的 properties 类生效并使用。如让 RedisProperties 生效并注册到 Spring 容器中,并且使用!

3)@ConditionalOnMissingBean当前 Spring 容器中没有该 bean(name 或者 class 指定),则注册该 bean 到 spring 容器。

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration(){}
@Bean
@ConditionalOnMissingBean(
    name </span>= {"redisTemplate"<span style="color: rgba(0, 0, 0, 1)">}
)
</span><span style="color: rgba(0, 0, 255, 1)">public</span> RedisTemplate&lt;Object, Object&gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> UnknownHostException {
    RedisTemplate</span>&lt;Object, Object&gt; template = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> template;
}

@Bean
@ConditionalOnMissingBean
</span><span style="color: rgba(0, 0, 255, 1)">public</span> StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> UnknownHostException {
    StringRedisTemplate template </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> template;
}

}

 由此可见 Spring Boot 对于自动配置的灵活性,可以传递给我们一种信息,Spring Boot 是如何灵活处理自动配置的?

往大了说,就是你可以选择约定使用大量默认配置简化开发过程,你也可以自定义开发过程中需要的自动配置。往细了说,就是通过各式各样的条件注解注册需要的 Bean 来实现灵活配置。

那么实现一个自动配置有哪些重要的环节呢?

继续往下看第二部分!

 

二、Spring Boot 自动配置的重要组成部分

 

  上面只是对自动配置中几个重要的注解做了简单介绍,但是并没有介绍要开发一个简单的自动配置需要哪些环节(部分),同样地我们还是以 RedisAutoConfiguration 与 RabbitAutoConfiguration 为例,主要查看注解头:

RedisAutoConfiguration:

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})

RabbitAutoConfiguration:

@Configuration
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
@EnableConfigurationProperties({RabbitProperties.class})
@Import({RabbitAnnotationDrivenConfiguration.class})

你会发现通常的一般的 AutoConfiguration 都有@Configuration,@ConditionalOnClass,@EnableConfigurationProperties,@Import 注解:

  • @Configuration:自然不必多说,表示是配置类,是必须存在的!
  • @ConditionalOnClass:表示该配置依赖于某些 Class 是否在 ClassPath 中,如果不存在那么这个配置类是毫无意义的。也可以说是该 class 在配置类中充当重要的角色,是必须存在的!。在后面的自定义 spring boot starter 中我们可以简单统称为服务类
  • @EnableConfigurationProperties:读取配置文件,需要让相关的 Properties 生效,每个自动配置都离不开 properties 属性,是必须存在的!
  • @Import:导入配置类中需要用到的 bean,通常是一些配置类。表示此时的自动配置类需要基于一些配置类而实现自动配置功能。当然不是必须的,有些是没有这个注解的,是不需要基于某些配置类的!

那么,可以简单总结得到一个自动配置类主要有以下几部分组成(单纯是根据个人理解总结出来的,没有官方说明)

  • properties bean 配置属性:用来读取 spring 配置文件中的属性,@EnableConfigurationProperties 与 @ConfigurationProperties 结合使用,具体请看下一篇实践 stater 例子。
  • 该配置类依赖的一些 Class,使用 @ConditionalOnClass 判断;
  • 该配置类依赖的一些配置 Bean,使用 @Import 导入。
  • 可能还有加载顺序的控制,如@AutoConfigureAfter,@AutoConfigureOrder 等
  • 一些 Bean 的加载,往往通过方法返回 Object,加 @Bean 以及一些条件注解来实现

======================================== 未完待续(下一篇补充自定义 starter 会涉及自动配置组成部分)==========================================