springboot starter 原理解析及实践
starter是springBoot的一个重要部分。通过starter,我们能够快速的引入一个功能,而无需额外的配置。同时starter一般还会给我提供预留的自定配置选项,我们只需要在application.properties中设置相关参数,就可以实现配置的个性化。
那么这些方便的操作是怎么实现的呢?通过了解其原理,我们也可以做一个自己的starter,来让别人快速使用我们的功能。
按个人理解,我认为springBoot Starter就是一个 智能化的配置类 @Configuration 。
接下来介绍内容包括:
1、【创建module】,首先我们自定义一个starter的module,根据你的starter实现复杂度,引入相关spring组件。最基本的,我们只需引入 spring-boot-autoconfigure 模块。
2、【业务bean实现】实现我们的业务bean,案例中我们实现最简单的sayHello服务,输入msg,返回“hello,{msg}”。
3、然后就是Configuration类的创建,这个类是starter自动初始化的核心类,负责把业务相关的bean智能的加载进来。
4、配置 spring.factories ,通过该配置,才能让springboot来自动加载我们的Configuration类。具体原理我们稍后深入了解。
具体的,是在模块的 resources/META-INF 目录下,新建 spring.factories 文件。内容如下:
最后我们把上述模块单独执行以下install或者deploy,一个starter就做好了。
其他项目使用我们的starter就非常简单了:(1)引入starter依赖;(2)注入需要的service。
done!
回头再看上边的开发流程,有两个地方需要我们了解一下:
(1)如何让starter被自动识别加载:spring.factories里的EnableAutoConfiguration原理。
(2)如何实现自动加载的智能化、可配置化:@Configuration配置类里注解。
这里我们只简单的说一下大致的原理和流程,执行细节大家可以按照文章给出的思路自己去研读。
在SpringBoot的启动类,我们都会加上 @SpringBootApplication 注解。这个注解默认会引入 @EnableAutoConfiguration 注解。然后 @EnableAutoConfiguration 会 @Import(AutoConfigurationImportSelector.class) 。
AutoConfigurationImportSelector.class 的selectImports方法最终会通过 SpringFactoriesLoader.loadFactoryNames ,加载 META-INF/spring.factories 里的 EnableAutoConfiguration 配置值,也就是我们上文中设置的资源文件。
实际使用中,我们并不总是希望使用默认配置。比如有时候我想自己配置相关功能,有时候我想更改一下默认的服务参数。这些常见的场景Starter都想到了,并提供了如下的解决方案:
springboot starter提供了一系列的 @Conditional* 注解,代表什么时候启用对应的配置,具体的可以去查看一下springboot的官方文档。
比如我们案例中的 「@ConditionalOnClass(DemoHelloService.class)」,代表如果存在DemoHelloService类时,配置类才会生效;又比如「@ConditionalOnMissingBean(DemoHelloService.class)」,代表着如果项目中没有DemoHelloService类型的bean,那么该配置类会自动创建出starter默认的DemoHelloService类型bean。
这个注解主要是为了解决如下场景:我想要使用starter的默认配置类,但是又想对配置中的某些参数进行自定义配置。 @ConfigurationProperties 类就是做这个工作的。例如上述例子中,我想对默认的defaultMsg做些个性化的设置。就可以按如下方式来实现:
starter新增ConfigurationProperties类bean
启用property
在实际项目中自定义默认msg
SpringBoot Starter运行原理代码解析
springboot-boot-starter:就是springboot的场景启动器。springboot将所有的功能场景都抽取出来,做成一个个的starter,只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来,根据公司业务需求决定导入什么启动器即可。
查看 @SpringBootApplication
springboot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置的工作:spring.factories文件位于springboot-autoconfigure.jar包中。
所以真正实现是从classpath中搜寻所有的 META-INF/spring.factories 配置文件,并将其中对应org.springframework.boot.autoconfigure.包下的配置项通过反射实例化为对应标注了@Configuration的JavaConfig形式的IOC容器配置类,然后将这些都汇总称为一个实例并加载到IOC容器中。
Springboot自定义xml文件解析
有时候,要通过自定义XML配置文件来实现一些特定的功能。这里通过例子来说明。
首先,看部分spring加载bean文件的源码:
spring-beans-5.0.6.RELEASE.jar!/org/springframework/beans/factory/xml/PluggableSchemaResolver.class :
spring-beans-5.0.6.RELEASE.jar!/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.class :
可以看出,spring在加载xml文件的时候,会默认读取配置文件 META-INF/spring.schemas 和 META-INF/spring.handlers 。这样,我们就可以在这两个文件添加我们自定义的xml文件格式和xml文件解析处理器。
新建一个Springboot工程,pom如下。
SelfDefineXmlTrial/pom.xml :
然后,新建一个用于测试controller。
com.lfqy.springboot.selfdefxml.controller.SelfDefXmlController :
最后,创建一个Springboot的启动类。
com.lfqy.springboot.selfdefxml.SelfDefXmlApplication :
运行启动之后,浏览器访问 效果如下:
修改前面提到的配置文件 META-INF/spring.schemas 、 META-INF/spring.handlers ,添加xml格式说明。
META-INF/spring.schemas :
META-INF/spring.handlers :
添加xml格式说明配置文件。
META-INF/selfdef.xsd :
添加自定义xml格式处理器类。
com.lfqy.springboot.selfdefxml.selxmlparse.UserNamespaceHandler :
新增xml格式解析类。
com.lfqy.springboot.selfdefxml.selxmlparse.UserBeanDefinitionParser :
新增自定义xml对应的bean类。
com.lfqy.springboot.selfdefxml.beans.User :
添加自定义xml配置文件读取的相关逻辑。
com.lfqy.springboot.selfdefxml.SelfDefXmlApplication :
到这里,编码就完成了,工程的目录结构如下。
运行之后,控制台输出如下:
这里,通过实现一个启动时自动初始化的一个servlet来实现。
com.lfqy.springboot.selfdefxml.servlet.StartupServlet :
在启动时加载servlet,为了方便区分,这里新写一个启动类。
com.lfqy.springboot.selfdefxml.SelfDefXmlLoadOnStartupApplication
到这里,编码已经完成,工程的目录结构如下:
运行之后,控制台输出如下:
Springboot初始化流程解析
以上是一个最简单的Springboot程序(2.0.3版本)示例,也是我们最通用的写法,但其中其实封装这一系列复杂的功能操作,让我们开始逐步进行分析。
首先这里最重要的必然是注解 @SpringBootApplication
@SpringBootApplication 注解由几个注解复合组成,其中最主要的就是 @SpringBootConfiguration 、 @EnableAutoConfiguration 和 @ComponentScan 这三个。
其中的 @ComponentScan 是spring的原生注解, @SpringBootConfiguration 虽然是springboot中的注解,但其实质就是包装后的 @Configuration ,仍然是spring中的注解,用于代替xml的方式管理配置bean
@EnableAutoConfiguration 的定义如上,这里最重要的注解是 @Import ( @AutoConfigurationPackage 注解的实现也是基于 @Import ),借助 @Import 的帮助,将所有符合自动配置条件的bean定义加载到IoC容器中。关于 @EnableAutoConfiguration 注解后续涉及到时会再详细说明。这里我们先回到启动类的 run 方法从头分析初始化流程。
可以看到'run'方法最终调用的是 new SpringApplication(primarySources).run(args) ,这里首先创建了 SpringApplication 对象,然后调用其 run 方法
这里主要是为 SpringApplication 对象进行初始化,这里要专门提一下的是 webApplicationType 和 getSpringFactoriesInstances 。
它用来标识我们的应用是什么类型的应用,来看一下 deduceWebApplicationType() 方法的实现
其返回值是 WebApplicationType 类型的枚举类,其值有 NONE 、 SERVLET 、 REACTIVE 三种,分别对应非WEB应用,基于servlet的WEB应用和基于reactive的WEB应用。
这里的核心是 SpringFactoriesLoader.loadFactoryNames(type, classLoader) 方法,来看一下
重点关注一下 loadSpringFactories(classLoader) 做了什么
这里的 FACTORIES_RESOURCE_LOCATION 定义为 META-INF/spring.factories ,因此该方法会扫描所有包下的该文件,将其解析成map对象并缓存到 cache 中以避免重复加载,springboot包下该文件的部分片段如下
从这里可以看出, setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)) 和 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 分别对应设置的是上述这些类。
解析完成后调用 createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names) 处理解析结果,生成对应的实例,源码如下
这里的核心是通过 ClassUtils.forName(name, classLoader) 方法,以反射的方式生成类实例 instanceClass 。由此可以看出 SpringFactoriesLoader.loadFactoryNames(type, classLoader) 的作用就是将 META-INF/spring.factories 中配置的内容进行实例化的工厂方法类,具备很强的扩展性,与SPI机制有异曲同工
的效果。
看完 SpringApplication 的初始化,接着跳回 run 方法继续分析
这里挑其中比较重要的几个方法进行分析
通过 getOrCreateEnvironment() 方法创建容器环境
可以看到 environment 存在则不会重复创建,当应用类型为servlet时创建的是 StandardServletEnvironment 对象,否则创建 StandardEnvironment 对象。
接着来看 configureEnvironment(environment, applicationArguments.getSourceArgs())
configurePropertySources(environment, args) 加载启动命令行的配置属性,来看一下实现
这里的 MutablePropertySources 对象用于存储配置集合,其内部维护了一个 CopyOnWriteArrayList 类型的list对象,当默认配置存在时,会向该list的尾部插入一个 new MapPropertySource("defaultProperties", this.defaultProperties) 对象。
接着来看 configureProfiles(environment, args)
这里主要做的事情就是获取 environment.getActiveProfiles() 的参数设置到 environment 中,即 spring.profiles.active 对应的环境变量。
最后来看一下 listeners.environmentPrepared(environment)
这里的 listeners 就是之前通过 META-INF/spring.factories 注册的所有listeners,后面我们先以其中最重要的 ConfigFileApplicationListener 做为例子进行分析,接着来看 listener.environmentPrepared(environment)
可以看到这里创建了一个 ApplicationEnvironmentPreparedEvent 类型的事件,并且调用了 multicastEvent 方法,通过该方法最终会调用到listener的 onApplicationEvent 方法,触发事件监听器的执行。
接下来具体看一下 ConfigFileApplicationListener 的 onApplicationEvent 方法做了什么
可以看到当监听到 ApplicationEnvironmentPreparedEvent 类型的事件时,调用 onApplicationEnvironmentPreparedEvent( (ApplicationEnvironmentPreparedEvent) event) 方法
可以看到这里通过 loadPostProcessors() 方法加载了 META-INF/spring.factories 中的所有 EnvironmentPostProcessor 类到list中,同时把 ConfigFileApplicationListener 自己也添加进去了。接着遍历list中所有对象,并执行 postProcessEnvironment 方法,于是接着来看该方法
这里的核心是 new Loader(environment, resourceLoader).load() ,这里的 Loader 是一个内部类,用于处理配置文件的加载,首先看一下其构造方法
可以看到这里的 resourceLoader 又是通过 SpringFactoriesLoader 进行加载,那么来看看 META-INF/spring.factories 中定义了哪些 resourceLoader
从名字就可以看出来, PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 分别用于处理.properties和.yml类型的配置文件。
接着来看看 load() 方法做了什么
initializeProfiles() 进行了 profiles 的初始化,默认会添加 null 和 default 到 profiles 中, null 对应配置文件application.properties和application.yml, default 对应配置文件application-default.yml和application-default.properties,这里的 null 会被优先处理,由于后处理的会覆盖先处理的,因此其优先级最低。
接着来看 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)) 方法
这里重点是通过 getSearchLocations() 获取配置文件的路径,默认会获得4个路径
接着会遍历这些路径,拼接配置文件名称,选择合适的yml或者properties解析器进行解析,最后将结果添加到 environment 的 propertySources 中。
可以看到这里也是根据 webApplicationType 的取值,分别创建不同的返回类型。
这里的 sources 装的就是我们的启动类,然后通过 load(context, sources.toArray(new Object[0])) 方法进行加载
来看一下 loader 是如何被加载的
经过一系列调用之后最终由 load(Class? source) 方法执行,这里比较有趣的是当Groovy存在时居然是优先调用Groovy的方式进行加载,否则才走 this.annotatedReader.register(source) 方法将启动类注册到 beanDefinitionMap 中。
这个 refresh() 方法相当重要,尤其是 invokeBeanFactoryPostProcessors(beanFactory) ,这是实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键部分,后续再详细讲解。
至此Springboot的启动流程已经大体分析完了,也了解了配置文件和启动类分别是是如何被加载的,但仍有两个问题待解,一是Springboot的核心思想约定大于配置是如何做到的,二是Springboot的各种spring-boot-starter-*是如何发挥作用的,这两个问题留待后续文章继续分析。
Springboot 读取配置文件原理
Springboot 读取配置文件(application.yaml, application.properties)的过程发生在SpringApplication#prepareEnvironment() 阶段,而prepareEnvironment又属于整个Springboot 应用启动的非常前置阶段,因为Environment的准备是后续bean创建的基础。让我们来一探启动是的详细code。除去StopWatch这些code,可以发现prepareEnvironment 发生在SpringApplication#run 这在整个应用启动的多步实质性操作中几乎是第一步。
而prepareEnvironment中最重要的是通过触发listener(EventPublishingRunListener)来通过SimpleApplicationEventMulticaster#multicastEvent发出ApplicationEnvironmentPreparedEvent。
而SimpleApplicationEventMulticaster#multicastEvent的实现其实也很简单,找到相关的监听ApplicationEnvironmentPreparedEvent的listener,然后一个个的调用他们的Listener#onApplicationEvent(event)方法,而这其中就包括了处理configuration文件的listener。
在Springboot 2.4.0 之前这个处理configuration 文件的lister是ConfigFileApplicationListener,在2.4.0之后,处理configuration 文件的lister是EnvironmentPostProcessorApplicationListener,并且对configuration文件的加载做了较大的改变,导致一些行为可能出现了变化,这也就是下面要详细讲的内容。
Springboot 2.4.0之后,configuration 文件的load顺序按照优先级是如下顺序(序号大的会被小的覆盖):
和之前版本比较,整体的属性加载顺序并无调整,只有Application properties(14,15)这里有顺序的调整,具体调整为:
如果存在多个active的profiles,例如[Test, Dev], 那么对于同时存在两个profile 配置文件中的配置,后面的profile里的配置(Dev)会覆盖前面profile(Test)里配置的值。
前面讲了这么多,终于要引出Springboot 2.4之后配置文件加载的行为变化了。
考虑这样的情况,如果我想在跑Springboot test的时候指定特定的profile,那么可以在Test class中加入@ActiveProfile("Test")。 如果我的应用中存在ApplicationEnvironmentPreparedEvent的某个自定义listener中,会根据当前environment 设置profile,如env.addActiveProfile("Dev")。
当前就会有两个active profile,由于springboot-test会在调用application#run 前利用DefaultActiveProfilesResolver把@ActiveProfile注解定义的profile(Test)先加入了active的profile,等test run的时候 env.addActiveProfile("Dev") 又会把"Dev"也作为active profile 加入,这时候当前的active profile便为["Test", "Dev"]。
据上面介绍,后面的profile(Dev)对应的configuration 会覆盖前面的(Test)。可Springboot 2.4.0之前的版本为我们做了调整,让Test class中@ActiveProfile内定义的profile所对应的配置文件成为最高优先级。
刚才提到在Springboot 2.4.0 之前这个处理configuration 文件的lister是ConfigFileApplicationListener,我们
来看看ConfigFileApplicationListener的相关code。
查看initializeProfiles(),发现此时对profile的顺序做了调整,将activatedViaProperty (Test) 放在最后add,于是profile的顺序就变成了[Dev, Test]。
在profiles.poll()时原本profile的顺序已经倒了过来,已经变为[Dev, Test], 在load()方法中由于后置的Test profile,application-Test.yaml中的值最终生效了。
可是到了Springboot2.4.0之后,ConfigFileApplicationListener被deprecated了,取而代之的是EnvironmentPostProcessorApplicationListener,EnvironmentPostProcessorApplicationListener通过调用ConfigDataEnvironmentPostProcessor来完成configuration加载。
EnvironmentPostProcessorApplicationListener.java
ConfigDataEnvironmentPostProcessor.java
ConfigDataEnvironmentPostProcessor只是老老实实的set了active profile,并没有调换profile的顺序。最后调用定义在spring.factories中的resource loader class来load 配置文件。
YamlPropertySourceLoader.java
插一句,Springboot为我们提供了很好的yaml文件parse的code,当你需要解析yaml文件时不妨直接参考Springboot的YamlPropertySourceLoader
这样一旦应用升级到Springboot 2.4.0之后相同的test code会使用application-Dev.yaml中配置的值,造成了test结果的改变。
如果要解决这个问题,根据上面介绍的配置文件优先级顺序,可以在@SpringbootTest中设置properties 来作为最终的配置覆盖当前profile对应的配置。
了解一个框架很不容易,一个小小的变化都有可能造成应用的行为变化,唯有刨根问底,不断总结才是framework人解决一切问题的不变的方法论。
SpringBoot 配置文件详解(告别XML)
快速学会和掌握 SpringBoot 的 核心配置文件的使用。
SpringBoot 提供了丰富的 外部配置 ,常见的有:
其中核心配置文件我们并不陌生,主要以Key-Value的形式进行配置,其中属性Key主要分为两种:
在 application.properties 添加配置如下:
① 添加数据源信息
在 application.propertis 添加配置如下:
① 添加认证信息,其中 socks.indentity.* 是自定义的属性前缀。
② 添加随机值,其中spring.test.* 是自定义的属性前缀。
使用方法: @ConfigurationProperties(prefix = "spring.datasource")
使用说明:提供 Setter方法 和 标记组件 Component
如何验证是否成功读取配置?答:这里可以简单做个验证,注入 MyDataSource ,使用 Debug 模式可以看到如下信息:
使用方法: @Value("spring.datasource.*")
使用说明:提供 Setter方法 和 标记组件 Component
注意事项:@Value不支持注入静态变量,可间接通过Setter注入来实现。
关于两者的简单功能对比:
显然,前者支持松绑定的特性更强大,所以在实际开发中建议使用@ConfigurationProperties来读取自定义属性。
SpringBoot 默认会加载这些路径加载核心配置文件,按优先级从高到低进行排列:具体规则详见 ConfigFileApplicationListener
如果存在多个配置文件,则严格按照优先级进行覆盖,最高者胜出:
举个简单的例子,例如再上述位置都有一个application.properties ,并且每个文件都写入了server.port=xx (xx分别是9001,9002,9003,9004),在启动成功之后,最终应用的端口为:9004。图例:
如果想修改默认的加载路径 或者 调改默认的配置文件名,我们可以借助命令行参数进行指定,例如:
YAML是JSON的一个超集,是一种可轻松定义层次结构的数据格式。
答: 因为配置文件这东西,结构化越早接触越规范越好。这里推荐阅读阮一峰老师写的 YAML语言教程 ,写的很简单明了。
引入依赖: 在POM文件引入 snakeyaml 的依赖。
使用说明: 直接在类路径添加 application.yml 即可。
例如下面这两段配置是完全等价的:
① 在 application.yml 配置数据源:
② 在 application.properties 配置数据源:
在项目的实际开发中,我们往往需要根据不同的环境来加载不同的配置文件。例如生产环境,测试环境和开发环境等。此时,我们可以借助 Profiles 来指定加载哪些配置文件。例如:
温馨提示:如果spring.profiles.active指定了多个配置文件,则按顺序加载,其中最后的优先级最高,也就是最后的会覆盖前者。
使用方法:
使用Maven插件打包好项目,然后在当前路径,执行DOS命令: java -jar demo.jar --server.port=8081 ,在控制台可看到应用端口变成了8081。
实现原理:
默认情况下,SpringBoot会将这些命令行参数转化成一个 Property ,并将其添加到 Environment 上下文。
温馨提示:
由于命令行参数优先级非常之高,基本高于所有常见的外部配置,所以使用的时候要谨慎。详见 PropertySource 执行顺序 。
关闭方法:
如果想禁用命令行属性,可以设置如下操作:springApplication.setAddCommandLineProperties(false)