说说SpringBoot是如何实现自动装配的

Spring Boot是Spring家族中的新宠,它不仅继承了Spring框架原有的优秀特性,还通过简化配置来进一步简化Spring应用程序的创建和开发过程。SpringBoot框架中有两个最主要的策略:开箱即用和约定优于配置。

  • 开箱即用:在开发过程中,通过引入maven依赖包,然后使用注解来代替繁琐的XML配置文件来管理对象的生命周期,这让开发人员摆脱了复杂的配置和包依赖管理的工作,更加专注于业务逻辑。
  • 约定优于配置:按约定编程是一种软件设计范式,系统、类库、框架应该假定合理的默认值,而非要求提供不必要的配置,从而既能获得配置简单的好处,而又不失灵活性。

有关SpringBoot的概念就不说太多了,可以查看一下官方文档。

自动配置

SpringBoot的自动配置乍一看很神奇,其实原理非常简单,实现自动配置的核心就是@Conditional注解。

一、@Condition是什么

@Condition是Spring4的一个新特性,注解的注释第一句写到“表明仅当所有组件都符合注册条件时,该组件才具有注册资格”,所以我们可以根据这个注解动态的决定需要加载的Bean。

例如我们想要根据不同的环境加载不同的类,我们可以通过spring.profiles.active=dev指定当前环境,创建类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class AppConfig {
@Bean
@Profile("dev")
public PropConfig devDataSource() {
}

@Bean
@Profile("pre")
public PropConfig preDataSource() {
}

@Bean
@Profile("prd")
public PropConfig prdDataSource() {
}
}

我们这里用到了@Profile注解,这个注解是spring3.1之后版本中提供的,从注解的定义上我们可以看到它也是一个Conditional,可以通过ConfigurableEnvironment#setActiveProfiles方法和spring.profiles.active配置完成设置,当然还有其他方法,这里就不一一写出了,详情可以查看类org.springframework.core.env.AbstractEnvironment

在业务复杂的情况下,可以使用@Conditional注解来提供更加灵活的条件判断,在SpringBoot中的的很多CccConfiguration类上都设置了很多的Conditional,整理后发现大致有以下几种:

  1. @ConditionalOnClass:当classpath下存在指定的类时,加载被注解的类,使用方法@ConditionalOnClass({A.class, B.class, C.class})
  2. @ConditionalOnBean:当Spring容器中存在指定的Bean实例时,加载被注解的类,使用方法@ConditionalOnBean({A.class, B.class})
  3. @ConditionalOnMissingBean:当Spring容器中不存在指定的Bean实例时,加载被注解的类,使用方法@ConditionalOnMissingBean({A.class, B.class, C.class})
  4. @ConditionalOnMissingClass:当classpath下不存在指定的类时,加载被注解的类,使用方法@ConditionalOnMissingClass({"cc.lu.A", "cc.lu.B"})
  5. @ConditionalOnProperty:控制某个configuration是否生效。具体操作是通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值,如果该值为空,则返回false;如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。如果返回值为false,则该configuration不生效;为true则生效,使用方法@ConditionalOnProperty(prefix="cc.lu.config", name="enable", havingValue="true"),上面的@Profile("dev")对等于@ConditionalOnProperty(name="spring.profiles.active", havingValue="dev")

还有很多常用到的注解,可以到org.springframework.autoconfigure.condition包内了解一下,每一个注解单独拿出来都可以讨论半天。下面我们写一个简单的例子:当classpath路径中存在cc.lu.A类、容器中不存在cc.lu.B类且存在配置cc.lu.config.auto=true时加载cc.lu.Cc

1
2
3
4
5
6
7
8
9
10
11
12
13
package cc.lu;

@ConditionalOnClass(A.class)
@ConditionalOnMissingBean(B.class)
@ConditionalOnProperty(prefix="cc.lu.config", name="auto", havingValue="true", matchIfMissing=false)
public class Cc {

public Cc() {
// 在构造器中打印一句话来校验构造器是否被调用
System.out.println("Cc init......");
}

}

二、CccAutoConfiguration分析

上面了解了@Conditional注解的机制,也写了一个简单的例子,灵的同学应该已经能猜到SpringBoot是如何来实现自动配置的了,我们现在基于2.2.6版本的源码来粗略的看一下。

我们创建一个SpringBoot工程,idea可以通过Spring Initializr来快速的创建一个项目,然后我们看整个工程的入口,也就是Application.java(新创建的SpringBoot应用一般只有一个启动类)

1
2
3
4
5
6
7
8
@SpringBootApplication
public class CcApplication {

public static void main(String[] args) {
SpringApplication.run(CcApplication.class, args);
}

}

既然过程只有这么一个类,那么关键点就是@SpringBootApplicationSpringApplication#run了,我们先来看下注解@SpringBootApplication,这玩意儿放在启动类上是想要干啥。

1
2
3
4
5
6
7
8
9
10
11
@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 {
// ...
}

注意到这个注解上有一个@EnableAutoConfiguration,这个注解的目的是启用Spring应用程序上下文的自动配置,尝试猜测和配置可能需要的bean。再来瞜一眼这个注解的定义

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
}

哎,这个@Import注解把AutoConfigurationImportSelector这个类导入了进来,也就是说我们使用@EnableAutoConfiguration的时候,AutoConfigurationImportSelector类会自动被加载,那么是不是核心代码就是在这个类中了?(这样写貌似有点尬!)

AutoConfigurationImportSelector实现于ImportSelector,关键方法就是ImportSelector#selectImports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 加载META-INF/additional-spring-configuration-metadata.json文件,将配置信息加载到环境中,这里就是为什么配置有默认值的关键,可用到jar包里面查看一下
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 加载META-INF/spring.factories文件,并将org.springframework.boot.autoconfigure.EnableAutoConfiguration的值以列表的形式返回
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);
// 将需要加载的AutoConfiguration返回
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

// 校验是否开启了自动配置
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}

selectImports的源码可以看到它只做了两件事:加载默认的配置属性和返回所有的AutoConfiguration的类信息,通过方法调用链找到最终加载的方法是SpringFactoriesLoader#loadSpringFactories

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
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
// 获取classpath下所有的META-INF/spring.factories文件
Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION):ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 加载文件内容
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 将文件内容存在到Map中
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
// value是使用逗号分隔的,所以这里转换成数组,也就是把一碗米饭分成一粒粒的
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
}
}

SpringBoot为我们提供的配置类有一二百个,但是我们不可能每个工程都把它们全部引入。所以在自动装配的时候,会去classpath下面寻找,是否有对应的配置类。如果有配置类,则按条件注解 @Conditional或者@ConditionalOnProperty等相关注解进行判断,决定是否需要装配。如果classpath下面没有对应的字节码,则不进行任何处理。

我们到spring.factories文件中随便找一个AutoConfiguration类,比如org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

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
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

}

这个类被@ConditionalOnClass@EnableConfigurationProperties两个注解修饰。

  • @ConditionalOnClass(RedisOperations.class)的意思是当classpath路径下存在RedisOperations这个类的时候加载RedisAutoConfiguration,类RedisOperations在spring-data-redis.jar包中,这个包通过spring-boot-starter-data-redis的starter引入,所以在我们引入这个starter的时候就自动去加载了RedisAutoConfiguration,然后再类中又创建了两个Bean,创建的前提是容器中不存在这两个类的实例,如果我们自定义一个RedisTemplate的实例,RedisAutoConfiguration#redisTemplate方法就会失效。
  • @EnableConfigurationProperties(RedisProperties.class):在加载RedisAutoConfiguration的时候同步加载RedisProperties,RedisProperties中通过注解@ConfigurationProperties(prefix = "spring.redis")指定关联的配置信息,若没有配置则使用类中属性的默认值。

至此,容器中有了RedisTemplate的实例和StringRedisTemplate的实例,并且还使用了配置文件中我们设置的Redis相关配置。

总结

整个SpringBoot中,都是通过@Conditional注解的各种扩展来实现自动配置的,我们也可以完全利用这些注解去实现我们自己的starter。