前言

眼下SpringBoot算是比较热门的一块,手头的小项目正好也是SpringBoot,于是打算总结下SpringBoot的核心特性:自动配置。虽然这部分知识并不深,网上也是一搜一大把,不过个人感觉还是值得一写。本文内容基于SpringBoot 2.0.4版本。

缘由杂谈

新技术的流行总是因为解决了某个痛点而进入视野。Spring强大的解耦能力可以说塑造了一个“王朝”,一个框架能够对一种语言产生如此深刻的“纠葛”应该是非常少见了。随后就是各种组件的集成,几乎所有java系组件都能够找到与Spring整合的例子,而整合带来了大量的以XML为主要格式的配置文件,处理配置配置文件的时候,最常用的手法就是所谓的“CV大法“,痛点就来源于此。

为了减少繁杂的配置文件,从Spring3.0开始,出现了注解@Configuration用于定义配置类,用配置类的形式代替XML配置文件。配置类带来了不少好处,比如更加易读、可配置生效条件等,但是并没有完全取代配置文件,应该说是相辅相成,而且配置并没有减少,只是换了个形式。

既然配置时存在大量的复制粘贴,那么何不弄成自动配置和所谓的OOTB?想必SpringBoot构建的出发点与这种想法有相似的考量。

约定优于配置

配置少了并不代表配置消失了,简化往往伴随着约定,SpringBoot也是如此。SpringBoot的应用入口点就是注解了@SpringBootApplication的主类的main方法,初次使用时SpringBoot时往往会惊艳于简单的一个main方法就能跑起整个应用。作为框架来说,简单是优点,可配是核心,同样的,自动配置这个特性也是可配的。

简单描述下SpringBoot自动配置生效的过程:@SpringBootApplication实际为一个复合注解,包括了@Configuration@EnableAutoConfiguration@ComponentScan,都与自动配置有关,但开启自动配置主要由@EnableAutoConfiguration来完成。

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({EnableAutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
String[] excludeName() default {};
}

@Import注解注入了EnableAutoConfigurationImportSelector类,这个类使用SpringFactoriesLoader.loadFactoryNames方法读取 ClassPath 目录下面的 META-INF/spring.factories 文件,从中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# spring.factories文件一瞥

# ...
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# 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.CloudAutoConfiguration,\
# ....

SpringBoot就通过这种“约定”的方式读取所有的自动配置类。相当于框架的创建者或者自动配置包的创建者有能力“帮助”使用者在Spring容器中添加配置类。

自动配置类

具体的自动配置就由一个个自动配置类来完成,它们的名称都约定为XXXAutoConfiguration。

配置注解

本质上来说自动配置类仍然是一个配置类,原理也是配置类的原理,因此自动配置类上必然包括@Configuration这个注解。

条件注解

而自动配置的情况下,由于配置类是“推送”过来的,必须要限制配置类的生效条件和生效的先后顺序以及主次区分,确保对容器内容的控制。为了实现配置的可控性,基于Spring4.0新增加的注解@Conditional,SpringBoot提供了多个注解来描述配置生效条件,几个主要的条件注解解释如下:

注解 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 满足SpEL表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnWebApplication 当前是web环境

几个主要的顺序控制注解如下:

注解 作用(判断先后顺序)
@AutoConfigureBefore 指定在某个别的自动配置类之前进行配置
@AutoConfigureAfter 指定在某个别的自动配置类之后进行配置
@AutoConfigureOrder 指定自动配置的排序系数(越小越先执行)

属性注解

自动配置并不是完全消除配置文件,在自动配置的“过滤”下,配置文件可以专心指定参数类、选项类的属性内容,这些内容在面向对象思想的指导下又可以映射到所谓的配置属性类的成员值上。为了完成、简化这个映射,SpringBoot提供了注解@ConfigurationProperties,只要注解在配置类上就可以根据前缀将配置文件中的内容快速映射到实体中。

同时,为了增强自动配置类的可读性和属性类实体快速加入容器,SpringBoot同时提供了注解@EnableConfigurationProperties,注解在自动配置类上,快速将属性类引入容器,配置类就可以被各个组件灵活使用。

引入注解

如果组件比较庞大,或者说组件包含了各种可定制使用的子模块,也可以将功能配置拆分在不同的配置类中,然后在自动配置类上统一使用注解@Import引入,以获取更清晰的层次结构和更好的阅读体验。使用@Import引入配置类时,配置类上的@Configuration不是必要的。

概览

以上几种注解组成了自动配置类的基本元数据,以HibernateJpa自动配置为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class })
@Conditional(HibernateEntityManagerCondition.class)
@EnableConfigurationProperties(JpaProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class })
@Import(HibernateJpaConfiguration.class)
public class HibernateJpaAutoConfiguration {

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
static class HibernateEntityManagerCondition extends SpringBootCondition {

private static final String[] CLASS_NAMES = {
"org.hibernate.ejb.HibernateEntityManager",
"org.hibernate.jpa.HibernateEntityManager" };

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("HibernateEntityManager");
for (String className : CLASS_NAMES) {
if (ClassUtils.isPresent(className, context.getClassLoader())) {
return ConditionOutcome
.match(message.found("class").items(Style.QUOTE, className));
}
}
return ConditionOutcome.noMatch(message.didNotFind("class", "classes")
.items(Style.QUOTE, Arrays.asList(CLASS_NAMES)));
}

}

}

启动器

为了达到快速自动配置的目的,还需要一个便捷的依赖引入方式,而这就是启动器(starter)的能力所在。启动器会把所有用到的依赖都给包含进来,避免了开发者自己去引入依赖所带来的麻烦。启动器构成了自动配置的最后一块拼图,引入启动器就可以使得一个组件进入可用状态。

官方启动器

SpringBoot官方提供了大量的自动配置类和启动器,整合了市面上的主流组件。

官方提供的自动配置类和属性类均在名为spring-boot-autoconfigure的包下,如果需要使用某项组件,在依赖管理工具中引入对应的starter即可,官方提供的启动器名称统一约定为spring-boot-starter-xxx。在自动配置类集中放置、starter按需引入的结构下,官方启动器本身不包含任何类只用作依赖的引入。

自定义启动器

如果SpringBoot官方没有提供对应的启动器,组件开发者也可以创建第三方自定义启动器。自定义启动器往往是一个独立的项目(jar包),本身引入、控制组件所需依赖,自动配置类和属性类也放在这个包下,并且创建对应的META_INF/spring.factories文件,这样使用者同样可以通过引入这个启动器达到自动配置的效果。第三方的启动器的名称一般约定为xxx-spring-boot-starter。

概览

自动配置本质上来说还是以@Configuration这个注解的功效作为主要表现形式,通过约定读取位置代替之前指定扫包路径的形式将配置类引入容器进行配置。通过@ConfigurationProperties绑定配置类和配置文件中的属性,可以快速定制一些基本的参数,并且提供默认的参数值。这样基本就达到了傻瓜式OOTB的效果。

丰富的条件注解可以使得在良好的设计下,容器内Bean的引入都是可控的,也能够支持深度定制,这也是为什么自动配置类总是注解了大量的@ConditionalOnMissingBean@ConditionalOnClass,包含了各种各样的Customizer、Adapter类的原因。

尽管自动配置原理并不复杂,但是依然可以看到各种各样的花式配置,套了一堆静态内部配置类让人摸不着头脑。把握好核心内容对框架的理解和应用是大有裨益的。