Featured image of post SpringBoot原理(二):Starter

SpringBoot原理(二):Starter

关于SpringBoot中Starter的原理探索

引言

SpringBoot内置了各种Starter的起步依赖,有了Starter我们不需要考虑项目需要什么库,版本会不会冲突等,大大减轻了我们的工作。

比如我们经典的web应用在引入spring-boot-starter-web后,不需要再考虑spring-webspring-webmvc及它们兼容的版本。

Maven的optional标签

关于RabbitMq在SpringBoot源码中有下面这样一个配置类,如下所示:

1
2
3
4
5
6
7
import org.springframework.amqp.rabbit.core.RabbitTemplate;

@Configuration
@ConditionalOnClass(RabbitTemplate.class)
public class RabbitHealthContributorAutoConfiguration {
    
}

省略了大部分代码,剩余一个条件注解@ConditionalOnClass,如不了解条件注解请阅读SpringBoot原理(一):自动配置 ,该注解表示 RabbitTemplate这个类存在时自动注入RabbitHealthContributorAutoConfiguration实例。

看到这里有些同学可能会有疑惑,既然该类可能不存在,直接使用RabbitTemplate.class,并使用import关键词导入的类路径不会爆红吗?打包项目时不会报错吗?

像这样书写@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")通过反射加载字符串判断类是否存在不是才正确吗?

先说答案,两种方式都是正确的,为什么第一种方式类路径不爆红呢,我们通过ide点进RabbitTemplate发现SpringBoot是引入了对应依赖的,那为什么还要判断该类是否存在呢,关键在于引入的方式,引入的pom依赖如下:

1
2
3
4
5
		<dependency>
			<groupId>org.springframework.amqp</groupId>
			<artifactId>spring-rabbit</artifactId>
			<optional>true</optional>
		</dependency>

该依赖在spring-boot-actuator-autoconfigure模块的pom中被引入,该依赖有一个<optional>true</optional>标签,该标签为true表示项目打包时不引入该依赖, 这就是可以使用且不暴红的原因。

显然易见,倘若使用到该处代码会发生notFoundClassException,因此要使用rabbitmq需主动引入相关依赖,引入依赖后再在application.yml配置参数后就能使用@Autowired获取对应实例。

通过上面一个例子我们说明了<optional>true</optional>标签的作用,<exclusions></exclusions>也有类似的功能。


Starter构建原理

我们可以看到SringBoot源码中spring-boot-build/spring-boot-project/spring-boot-starters目录下有大量的Starter,以最常用的spring-boot-starter-web起步依赖来说明。

spring-boot-build/spring-boot-project/spring-boot-starters/spring-boot-starter-web目录下没有代码,只有一个pom.xml,简略内容如下:

 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
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starters</artifactId>
		<version>${revision}</version>
	</parent>
	<artifactId>spring-boot-starter-web</artifactId>
	<name>Spring Boot Web Starter</name>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-json</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.apache.tomcat.embed</groupId>
					<artifactId>tomcat-embed-el</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
		</dependency>
	</dependencies>
</project>

可以看出,spring-boot-starter-web模块依赖了spring-boot-starter,spring-boot-starter-tomcat,spring-webspring-webmvc等模块。

spring-boot-starter模块是绝大部分spring-boot-starter-xxx模块依赖的基础模块。其中spring-boot-starter依赖了spring-boot-autoconfigure,因此 spring-boot-starter-web间接依赖了spring-boot-autoconfigure

@ConfigurationProperties

经过上面的学习,我们明白了Starter引入依赖就自动配置的原因是条件注解+<optional>true</optional>标签

只引入一个Starter依赖是因为Starter本身已配置好对应依赖,例如spring-boot-starter-web

这些都是maven的功能,Starter还有一项重要的能力,在application.yml中配置对应的参数,实例能自动获取到值并配置,比如我们在application.properties配置文件中配置server.port=8081, 该值会自动绑定到类ServerProperties的属性port上 。这是怎么实现的呢?可以推测与spring-boot-autoconfigure有很大的相关性。

细心的我们发现类ServerProperties上有注解@ConfigurationProperties,与之关联的注解有@EnableConfigurationProperties,接下来我们研究下两个注解的原理。


@ConfigurationProperties和@EnableConfigurationProperties

ServerProperties的源码如下:

1
2
3
4
5
6
7
8
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

	private Integer port;

	private InetAddress address;
	//......
}

可以看到ServerProperties类上标注的@ConfigurationProperties注解,配置前缀为serverignoreUnknownFields是否忽略未知值为true

注解@ConfigurationProperties的源码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {

	@AliasFor("prefix")
	String value() default "";

	String prefix() default "";

	boolean ignoreInvalidFields() default false;
    
	boolean ignoreUnknownFields() default true;
}

该注解有四个字段,其中valueprefix的别名,ignoreInvalidFields表示忽略无效的配置,默认值为false。

@ConfigurationProperties这个注解的作用就是将外部配置的配置值绑定到其注解的类的属性上,可以作用于配置类或配置类的方法上。

可以发现这个注解没有任何处理逻辑,是一个标志性注解,代码调用入口不在这里。端口和地址是属于服务器配置,与之关联的代码为:

1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
//......
public class ServletWebServerFactoryAutoConfiguration {
//......
}

这是一个ServletWeb相关的配置类,对应的条件注解我们也能明白其作用,重点不在这里,该类上有一个@EnableConfigurationProperties注解,它的value值就是我们的服务器参数 配置类ServerProperties.class,这个注解的作用应该就是为@ConfigurationProperties注解绑定值提供支持。它是如何起作用的呢?

@EnableConfigurationProperties的源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {

	String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

	Class<?>[] value() default {};
}

该注解上有一个重要注解@Import(EnableConfigurationPropertiesRegistrar.class),对于这种代码是否有些熟悉呢,如果你详细了解 SpringBoot原理(一):自动配置@SpringBootApplication注解 那部分。

@Import导入的类EnableConfigurationPropertiesRegistrar的源码为:

1
2
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
}

ImportBeanDefinitionRegistrar接口是SpringBoot对外提供的扩展接口,被导入时会调用该接口的方法,我们知道一个普通的bean被配置到容器有两个大的步骤, 第一步被@Component@beanImport注解的类和META-INF/spring.factories中配置的类会被包装成BeanDefinition实例存储到beanFactory中, 只有一些特殊的bean比如后置处理器会被提前实例化;第二步调用最后的finishBeanFactoryInitialization方法才是将BeanDefinition实例实例化为真正的bean

ImportBeanDefinitionRegistrar接口会在第一步扫描到配置类时注册为BeanDefinition实例,同时也会获取该类上的@Import导入的ImportBeanDefinitionRegistrar实现类,并执行对应的方法, 可以在此时额外注册一些BeanDefinition实例(如果你对这部分感兴趣,请继续阅读SpringBoot原理系列文章)

回到代码,这里在将x注册为BeanDefinition实例时,会执行实现类EnableConfigurationPropertiesRegistrarregisterBeanDefinitions方法,对应源码为:

1
2
3
4
5
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		registerInfrastructureBeans(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
		getTypes(metadata).forEach(beanRegistrar::register);
	}

这里就不进行具体分析,该方法首先注册一个后置处理器BeanPostProcessor的实例ConfigurationPropertiesBindingPostProcessor,然后会扫描项目中的@EnableConfigurationProperties注解,例如@EnableConfigurationProperties(ServerProperties.class)等, 将它们注册成BeanDefinition实例

在将BeanDefinition实例真正实例化时会执行这个后置处理器为注解中的class的bean实例绑定值,如ServerProperties.class实例

真正绑定的逻辑在ConfigurationPropertiesBindingPostProcessor这里后置处理器中,它只重写了后置处理器的一个接口postProcessBeforeInitialization,初始化之前进行绑定,源码如下:

1
2
3
4
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
		return bean;
	}

核心就在这个bind方法,请自行分析

小结

通过上面的介绍我们了解了Starter特性的原理,但我们可能对两种不同的后置处理器BeanFactoryPostProcessorBeanPostProcessor在SpirngBoot启动过程中的调用时机, 不了解一个注解(@Component@bean等等)标识的类何时注册成BeanDefinition实例,何时将BeanDefinition实例变为真正的bean。

如果你对上诉问题感兴趣,请结合SpringBoot的源码和 SpringBoot原理(一):自动配置SpringBoot原理(三):启动流程分析SpringBoot原理(四):常用注解分析 进行调试学习。

Please call the seeds under the diligent.
Built with Hugo
主题 StackJimmy 设计