文本我们会一步一步做一个例子来看看SpringBoot的自动配置是如何实现的,然后来看一些SpringBoot留给我们的扩展点。
自己制作一个SpringBoot Starter
我们知道SpringBoot提供了非常多的启动器,引入了启动器依赖即可直接享受到自动依赖配置和自动属性配置:
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-project/spring-boot-starters
在第一篇文章中我提到,在SpringBoot出现之前,我们需要使用SpringMVC、Spring Data、Spring Core都需要对Spring内部的各种组件进行Bean以及Bean依赖的配置,在90%的时候我们用的是默认的配置,不会自定义任何扩展类,这个时候也需要由使用者来手动配置显然不合理,有了SpringBoot,我们只需引入启动器依赖,然后启动器就可以自己做为自己的一些内部组件做自动配置,大大方便了使用者。启动器的实现非常简单,我们来看下实现过程。
首先创建一个Maven空项目,引入SpringBoot:
<?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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.josephzhu</groupId>
<artifactId>spring101</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring101</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/><!-- lookup parentfrom repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后我们创建一个Starter模块隶属于父项目:
接下去我们创建一个服务抽象基类和实现,这个服务非常简单,会依赖到一些属性,然后会有不同的实现(无参构造函数设置了打招呼用语的默认值为Hello):
这里注入了自定义属性类:
这里看到了如果我们需要定义一个自定义类来关联配置源(比如application.properties文件配置)是多么简单,使用@ConfigurationProperties注解标注我们的POJO告知注解我们配置的前缀即可。额外提一句,如果希望我们的IDE可以针对自定义配置有提示的话(自动完成,而且带上注解中的提示语),可以引入如下的依赖:
这样编译后就会在META-INF下面生成一个叫做spring-configuration-metadata.json的文件:
之后在使用配置的时候就可以有提示:
我们先来写第一个服务实现,如下,只是输出一下使用到的一些自定义属性:
关键的一步来了,接下去我们需要定义自动配置类:
通过EnableConfigurationProperties来告诉Spring我们需要关联一个配置文件配置类(配置类斗不需要设置@Component),通过@Configuration告诉Spring这是一个Bean的配置类,下面我们定义了我们Service的实现。
最后,我们需要告诉SpringBoot如何来找到我们的自动配置类,在合适的时候自动配置。我们需要在项目资源目录建一个META-INF文件夹,然后创建一个spring.factories文件,写入下面的内容:
好了,就是这么简单,接下去我们创建一个项目来使用我们的自定义启动器来试试:
创建一个Runner来调用服务:
创建主程序:
然后在main模块的资源目录下创建application.properties文件,写入两个配置:
运行后可以看到输出:
2018-09-30 14:55:00.848 INFO 12704 --- [ main] me.josephzhu.spring101main.Runner1 : V1 Hello >> zhuye:35 !!
可以证明,第一我们的main模块引入的starter正确被SpringBoot识别加载,第二starter中的Configuration正确执行不但加载了配置类,而且也正确注入了Service的一个实现。
如何实现条件配置?
作为组件的开发者,我们有的时候希望针对环境、配置、类的加载情况等等进行各种更智能的自动配置,这个时候就需要使用Spring的Conditional特性。我们来看一个例子,如果我们的Service随着发展演化出了v2版本,我们希望用户在默认的时候使用v1,如果需要的话可以进行version属性配置允许用户切换到v2版本。实现起来非常简单,首先定义另一个v2版本的服务:
和版本v1没有任何区别,只是标记了一下v2关键字。
然后我们改造一下我们的自动配置类:
这里主要是为两个Bean分别添加了@ConditionalOnProperty注解,注解是自解释的。这里说了如果version的值是v1或没有定义version的话匹配到默认的v1版本的服务,如果配置设置为v2的话匹配到v2版本的服务,就这么简单。
再来看一个例子,如果我们的使用者希望自己定义服务的实现,这个时候我们需要覆盖自动配置为我们自动装配的v1和v2,可以使用另一个注解@ConditionalOnMissingBean来告知SpringBoot,如果找不到Bean的话再来自动配置:
这样的话,如果客户端自己定义了Service的实现的话,就可以让自动配置放弃自动配置使用客户端自己定义的Bean。还有N多的Conditional注解可以使用,甚至可以自定义条件,具体可以查看官方文档。
进行一下测试
接下去,我们来写一下单元测试来验证一下我们之前的代码,使用ApplicationContextRunner可以方便得设置带入的各种外部配置项以及自定义配置类:
在这里我们写了三个测试用例:
l 在提供了合适的属性配置后,可以看到服务的输出正确获取到了属性。
l 使用配置项version来切换服务的实现,在省略version,设置version为1,设置version为2的情况下得到正确的输出,分别是v1、v1和v2。
l 在客户端自定义实现(MyServiceConfig)后可以看到并没有加载使用自动配置里定义的服务实现,最后输出了打招呼用语Hi而不是Hello。
运行测试可以看到三个测试都可以通过,控制台也输出了hello方法的返回值:
实现自定义的配置数据源
接下去我们来看一下如何利用EnvironmentPostProcessor来实现一个自定义的配置数据源。我们在starter项目中新建一个类,这个类使用了一个Yaml配置源加载器,然后我们把加载到的自定义的PropertySource加入到PropertySource候选列表的第一个,这样就可以实现属性优先从我们定义的(classpath下的)config.yml来读取:
最关键的一步就是让SpringBoot能加载到我们这个PostProcessor,还是老样子,在spring,factories文件中加入一项配置即可:
现在,我们可以在starter项目下的resrouces目录下创建一个config.yml来验证一下:
重新运行main项目可以看到如下的输出结果中包含了yml字样:
2018-09-3015:27:05.123 INFO 12769 --- [ main]me.josephzhu.spring101main.Runner1 : V1 Hello >> zhuye_yml:35 !!
我们可以为项目添加一下Actuator模块进行进一步验证:
在配置文件中加入设置来放开所有的端口访问:
然后打开浏览器访问http://127.0.0.1:8080/actuator/env:
可以看到,的确是添加了我们自定义的config.yml作为PropertySource。
自动配置的调试问题
对于复杂的项目,如果我们发现自动配置不起作用,要搞清楚框架是如何在各种条件中做自动配置以及自动配置的匹配过程是比较麻烦的事情,这个时候我们可以打开SpringBoot的Debug来查看日志:
我们可以在日志中搜索我们关注的类型的匹配情况,是不是很直观呢:
MyAutoConfiguration#getMyService matched:
-@ConditionalOnProperty (spring101.version=v1) matched (OnPropertyCondition)
-@ConditionalOnMissingBean (types:me.josephzhu.spring101customstarter.MyService; SearchStrategy: all) did notfind any beans (OnBeanCondition)
MyAutoConfiguration#getMyServiceV2:
Did not match:
-@ConditionalOnProperty (spring101.version=v2) did not find property 'version'(OnPropertyCondition)
我们可以试试在出错的时候系统给我们的提示,来把配置文件中的version设置为3:
重新运行后看到如下输出:
***************************
APPLICATIONFAILED TO START
**************************
Description:
Field servicein me.josephzhu.spring101main.Runner1 required a bean of type'me.josephzhu.spring101customstarter.AbstractMyService' that could not befound.
- Bean method 'getMyService' in'MyAutoConfiguration' not loaded because @ConditionalOnProperty(spring101.version=v1) found different value in property 'version'
- Bean method 'getMyServiceV2' in'MyAutoConfiguration' not loaded because @ConditionalOnProperty(spring101.version=v2) found different value in property 'version'
Action:
Considerrevisiting the entries above or defining a bean of type'me.josephzhu.spring101customstarter.AbstractMyService' in your configuration.
这个所谓的分析报告是比较机械性的,作为框架的开发者,我们甚至可以自定义叫做FailureAnalyzer的东西来做更明确的提示。实现上和之前的步骤几乎一样,首先自定义一个类:
这里我们根据cause的Bean类型做了简单判断,如果发生错误的是我们的Service类型的话,告知使用者明确的错误原因(Description)以及怎么来纠正这个错误(Action)。
然后老规矩,在spring.factories中进行关联:
重新运行程序后可以看到如下的结果:
***************************
APPLICATIONFAILED TO START
***************************
Description:
加载MyService失败
Action:
请检查配置文件中的version属性设置是否是v1或v2
是不是直观很多呢?这里我的实现比较简单,在正式的实现中你可以根据上下文以及环境的各种情况为用户进行全面的分析,分析服务启动失败的原因。
SpringBoot的扩展点
在之前的几个例子中,我们进行了各种扩展配置,通过spring.factories进行了自动配置、环境后处理器配置以及错误分析器配置:
其实,SpringBoot还有一些其它的扩展槽,如下是SpringBoot自带的一些配置类:
我们可以看到一共有8大类的扩展槽,这些槽贯穿了整个SpringBoot加载的整个过程,感兴趣的读者可以逐一搜索查看Spring的文档和源码进行进一步的分析。
本文从如何做一个Starter实现自动配置开始,进一步阐述了如何实现智能的条件配置,如何,如何进行自动配置的测试,然后我们自定义了环境后处理器来加载额外的配置源(你当然可以实现更复杂的配置源,比如从Redis和数据库中获取配置)以及通过开启Actutor来验证,定义了配置错误分析器来给用户明确的错误提示。最后,我们看了一下spring.factories中的内容了解了SpringBoot内部定义的一些扩展槽和实现。
(本文代码见https://github.com/JosephZhu1983/Spring101)