logo头像

路漫漫兮其修远兮

实现一个Spring boot stater

1 自动配置

Spring boot的一大特性就是“自动配置”。在传统的Spring应用开发中,开发者往往需要写很多的XML配置项,包括数据源的配置,组件Bean的配置,数据库事务的配置等等,但如果使用Spring Boot的话,往往不需要做这些配置,只需要添加对应的依赖库即可,即所谓的“开箱即用”,这可以让应用开发者把更多的精力放在业务逻辑上,而不是一大堆的乱七八糟的配置上,对于这点,我想只要有传统SSM项目开发经验以及Spring Boot开发经验的朋友应该能体会到。

那Spring Boot自动配置的原理是什么呢?它又是如何实现的呢?

Spring Boot的自动配置关键是spring-boot-autoconfigure依赖,如果仔细观察,会发现Spring boot starter包含了该依赖,如下图所示(我这里用的2.0.0版本):

FZvAld.png

pring-boot-autoconfigure包含了很多自动配置项,例如JPA的自动配置,Kafka的自动配置等,如下图所示(仅截了部分):

FZx1UK.png

进一步查看源码,会发现每一个包下都有一个XXXAutoConfiguration(XXX表示就是组件名称,例如JPA,Kafka等),下面是KafkaAutoConfiguration的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ConditionalOnClass(KafkaTemplate.class)
@EnableConfigurationProperties(KafkaProperties.class)
@Import(KafkaAnnotationDrivenConfiguration.class)
public class KafkaAutoConfiguration {

private final KafkaProperties properties;

private final RecordMessageConverter messageConverter;

.........
}
  1. @Configuration注解表示这是一个配置类,用过Spring Boot的朋友都应该知道是什么东西,不多做解释了。
  2. @ConditionalOnClass(KafkaTemplate.class),该注解表示当KafkaTemplate.class被加载到JVM中,才会对配置类进行初始化,简单理解就是把他当做if语句来看。
  3. @EnableConfigurationProperties(KafkaProperties.class),加载属性配置类的注解,有这个注解,KafkaProperties才会作用于应用上下文中。
  4. @Import(KafkaAnnotationDrivenConfiguration.class),Spring 的基础注解,不多说了。

如果你用IDEA来查看该源码,且你的项目中没有包含Spring Kafka相关的依赖项,应该会看到KafkaTemplate被标红了,即IDE找不到该类,所以最终KafkaAutoConfiguration不会被初始化,项目中也不会存在KafkaTemplate这个Bean。而如果此时加入Spring Kafka的依赖项,那么KafkaAutoConfiguration就会被初始化,最终应用中会存在KafkaTemplate这个Bean,用户可以直接依赖注入到需要用到的地方而不需要做什么配置,因为KafkaAutoConfiguration这个类里都帮我们做了一些默认的配置。

如果项目确实对KafkaTemplate有什么特殊的配置,仍然可以选择自己手动配置,一般有两种方法:

  1. 自己创建一个KafkaTemplate的Bean,这个应该不难。在配置类中用@Bean注解即可。
  2. 在配置文件application.properties配置一些KafkaTemplate的配置项,这些配置项会覆盖默认配置。

那Spring Boot是如何发现这些东西的呢?也就是说Spring boot是怎么知道这是一个自动配置类的呢?主要有两个,一是@EnableAutoConfiguration注解,而是spring.factories配置文件,现在看看spring.factories配置文件,该文件在类路径中META-INF文件夹下,如下图所示:

FeSHBR.png

文件里有什么配置呢?打开看看就知道了,文件里的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置就是我们今天讨论的关键,可以发现,该键对应着很多值,每个值之间用逗号分隔(\是换行),搜索一下可以发现,存在KafkaAutoConfiguration这个配置项,完整的是org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration。

接下来来看看@EnableAutoConfiguration注解,该注解在@SpringBootApplication中有用到,所以只要加入了@SpringBootApplication注解,也就加入了@EnableAutoConfiguration注解,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

String[] excludeName() default {};

}

关键是@Import(AutoConfigurationImportSelector.class)注解,该注解导入了AutoConfigurationImportSelector这个类,该类是自动配置的核心,从名字上看可以看出这应该是一个选择器。其部分代码如下:

1
2
3
4
5
6
7
8
9
10
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
//加载spring.factories配置文件的配置信息
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

现在可以把上面说到的串起来了。

  1. 在应用的配置类上加入@EnableAutoConfiguration注解,该注解会导入AutoConfigurationImportSelector类。
  2. 启动应用的时候,Spring Boot会获取spring.factories里的配置信息。(AutoConfigurationImportSelector。getCandidateConfigurations()方法)
  3. 获取到信息之后,Spring Boot就会尝试去触发XXXAutoConfiguration,是否能触发还取决于具体AutoConfiguration类,例如在KafkaAutoConfiguration中,如果缺少KafkaTemplate类的存在,那么就不会触发KafkaAutoConfiguration的执行,如果触发成功,就会执行KafkaAutoConfiguration里的代码,这时候会发生什么就取决于具体的代码逻辑了。

简单概括就是以上三个流程。当然,其中的细节还有很多,例如Spring Boot是如何去读取spring.factories的配置信息的等等,所以我以上说的都只是大致原理,真正的实现其实异常复杂!!!

这里还要一说的是spring.factories文件并不仅仅包含了自动配置相关的配置信息,还包含其他一些信息,所以不要误以为spring.factories是专门为自动配置服务的。

2 动手实现一个Starter

Spring Boot系列有很多的starter,例如spring-boot-starter-web,spring-boot-starter-data-jpa等等,我们知道加入spring-boot-starter-web依赖,不需要任何配置,就可以直接构建Web应用了,这就是自动配置的威力。实际上,Spring作为一个扩展性比较强的框架,还允许用户自己编写符合需求的starter。

一个完整的starter组件至少需要包含两个部分:

  • 提供自动配置功能的自动配置模块。
  • 提供依赖关系管理功能的组件模块,即封装了组件所有功能,开箱即用。

具体的来说,就是需要一个XXXAutoConfiguration自动配置类以及一个封装好的功能模块,例如JPATemplate等等,用的时候直接依赖注入即可。

这时候可能有读者要问了,在spring-boot-starter-web项目里好像没发现什么XXXAutoConfiguration啊,其实是有的,和Web相关的自动配置类被Spring写到spring-boot-autoconfigure项目里了,即ServletWebServerFactoryAutoConfiguration。

spring-boot-autoconfigure里其实包含了很多XXXAutoConfiguration,例如KafkaAutoConfiguration,HibernateJpaAutoConfiguration等等。基本都是Spring家族的项目,但对于我们自制的starter就需要在项目中写XXAutoConfiguration类了,毕竟Spring boot不能提前预知所有用户的需求不是?好了,不多说了,直接动手实现吧!

首先构建一个Maven项目(其他的构建框架也可以,挑一个熟悉就行),pom文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>top.yeonon</groupId>
<artifactId>car-server-starter</artifactId>
<version>1.0</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>

</project>

其实只加入了spring-boot-autoconfigure依赖,因为比较简单嘛,不需要太多东西。

然后创建一个Properties类(其实没有也没什么关系,最好还是有,提供一些灵活性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ConfigurationProperties(prefix = "yeonon.car")
public class CarProperties {

private static final String DEFAULT_NAME = "BWM";
private static final String DEFAULT_COLOR = "white";
private static final Integer DEFAULT_AGE = 1;


private String name = DEFAULT_NAME;

private String color = DEFAULT_COLOR;

private Integer age = DEFAULT_AGE;

//getter 和 setter必须要有,否则属性注入会有问题
}

之后创建我们的功能模块:

1
2
3
4
5
6
7
8
9
10
11
12
public class CarService {

private Car car;

public Car getCar() {
return car;
}

public void setCar(Car car) {
this.car = car;
}
}

非常简单,就是获取Car对象而已。Car类就不贴出来了,就是一个POJO而已。下面就是核心类了,即CarAutoConfiguration自动配置类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@ConditionalOnClass(CarService.class)
@EnableConfigurationProperties(value = CarProperties.class)
public class CarAutoConfiguration {

@Autowired
private CarProperties carProperties;

@Bean
@ConditionalOnMissingBean(CarService.class)
@ConditionalOnProperty(value = "yeonon.car.server.configuration.auto.enabled", matchIfMissing = true)
public CarService carService() {
CarService carService = new CarService();
Car car = new Car();
car.setName(carProperties.getName());
car.setColor(carProperties.getColor());
car.setAge(carProperties.getAge());
carService.setCar(car);
return carService;
}
}
  1. @Configuration注解是要有的,否则无法生效。
  2. @ConditionalOnClass(CarService.class),原则上可有可无,但作为一个健壮的starter,还是有的好,作为一个“防御措施”。
  3. @EnableConfigurationProperties(value = CarProperties.class),要使用属性系统,就需要把属性类加入到应用上下文中。
  4. @Bean就是定义一个Bean了,代码逻辑没什么可说的,无法就是创建对象,然后构建对象并返回而已。关键在于@ConditionalOnMissingBean注解和@ConditionalOnProperty注解。@ConditionalOnMissingBean注解的意思就是如果应用中不存在CarService的Bean,那么就执行下面的方法构建一个Bean,已经存在的话,就不会调用下面的方法了,这意味着用户可以自己创建Bean来覆盖系统默认配置的Bean。@ConditionalOnProperty就是当配置存在的时候,才会执行Bean的构建。

打完收工!别着急!别忘了要配置spring.factories,spring-boot-autoconfigure里的spring.factories我们是没法动的,所以就只能在自己的项目中动刀子了。

在resource文件夹(其实就是类路径classpath)下创建一个META-INF文件夹,为什么要创建这玩意?问Spring去吧!然后创建spring.factories文件(不要打错一个字!)。在里面写入如下内容:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
top.yeonon.stater.test.CarAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration这个键名是不是很熟悉,没错,就是spring-boot-autoconfigure中的spring.factories里的键名,在这里我们只需要把我们自己的CarAutoConfiguration全限定类名加入就行了。

这才算完事,然后测试一下?先打包,打包过程我就不说了,然后新建一个项目,把刚刚打包好的car-starter作为依赖加入进去,如下所示:

1
2
3
4
5
<dependency>
<groupId>top.yeonon</groupId>
<artifactId>car-server-starter</artifactId>
<version>1.0</version>
</dependency>

为了方便测试,也加入spring-boot-starter-web吧,然后写一个Controller,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/hello")
public class HelloController {

@Autowired
private CarService carService;

@GetMapping("car")
public Car getMyCar() {
return carService.getCar();
}
}

运行一下,访问该路径,大致可以得到如下输出:

FeCaA1.png

这里的值还是我们之前的默认值(返回去看看CarProperties类),Spring Boot的属性系统也是相当厉害,现在来试试修改配置项,如下所示:

1
2
3
4
## 注意前缀是yeonon.car,也是在CarProperties类里配置好的
yeonon.car.name=MSLD
yeonon.car.age=2
yeonon.car.color=black

然后重启项目,再次访问,得到的结果应该是这样:

FeCIgS.png

是不是很神奇?

3 小结

Spring Boot的自动配置非常强大,免去了很多配置文件,用得好的话会觉得既方便又灵活,用不好的话可能会发生一些配置冲突的问题(不过其实都是能解决的)。自己编写stater也是可行的,不过要确定确实需要自定义的starter,否则最好还是不要给自己挖坑哈,例如我们的Car stater这个例子,实际上如果仅仅是为了实现这个功能,完全不需要专门写一个starter。

最后,Spring水很深,道阻且长!