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

  try {
    // 获取classpath下所有的META-INF/spring.factories文件
    Enumeration 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 redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		RedisTemplate 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。

你可能感兴趣的:(说说SpringBoot是如何实现自动装配的)