您现在的位置是:首页 >技术杂谈 >Spring Cloud Config配置服务及那些你不知道的坑网站首页技术杂谈
Spring Cloud Config配置服务及那些你不知道的坑
目录
1、为什么选择Spring Cloud Config
如何将配置信息直接写在本地yml配置文件存在哪些痛点呢?
如果多个微服务可能使用相同的配置信息,假设有50个微服务,如果配置需要修改配置文件,就意味着我们需要修改50个微服务的yml文件,极其浪费时间。配置信息修改后,必须重启服务才能生效。
相比较同类产品,SpringCloudConfig最大的优势是和Spring无缝集成,支持Spring里面Environment和PropertySource的接口,对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上是完全一致,结合SpringBoot可使你的项目有更加统一的标准(包括依赖版本和约束规范),避免了应为集成不同开软件源造成的依赖版本冲突。
而Spring Cloud Config解决了这两个痛点:
1.1 集中式管理
在开发中多个微服务可能使用相同的配置,假设有50个微服务,如果配置需要修改配置文件,就意味着我们需要修改50个微服务的yml文件。使用配置中心后,就可以做到一处修改,处处修改。
1.2 动态修改配置
使用配置中心,配合actuator可以实现配置的动态修改,无需重启服务
2、Spring Cloud Config 简介
SpringCloudConfig就是我们通常意义上的配置中心,把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。SpringCloudConfig分服务端和客户端,服务端负责将git svn中存储的配置文件发布成REST接口,客户端可以从服务端REST接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置,这需要每个客户端通过POST方法触发各自的/refresh。
SpringCloudBus通过一个轻量级消息代理连接分布式系统的节点。这可以用于广播状态更改(如配置更改)或其他管理指令。SpringCloudBus提供了通过POST方法访问的endpoint/bus/refresh,这个接口通常由git的钩子功能调用,用以通知各个SpringCloudConfig的客户端去服务端更新配置。
注意:这是工作的流程图,实际的部署中SpringCloudBus并不是一个独立存在的服务,这里单列出来是为了能清晰的显示出工作流程。
下图是SpringCloudConfig结合SpringCloudBus实现分布式配置的工作流:
简单说明一下流程:
1)把配置文件放在Git Repository中。
2)Config Server从Git repository中读取配置信息。
3)其他客户端再从Config Server中加载配置文件
3、服务端配置
紧接上一篇内容代码示例,创建一个新的module. 命名为:springcloud-config-server。
3.1 添加依赖
在springcloud-config-server pom.xml中添加config-server依赖。如下,
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
3.2 开启服务注册
新建启动类SpringCloudConfigServerApp,并添加@EnableConfigServer,表示开启 SpringCloudConfig配置。
代码示例如下,
package com.xintu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
/**
* @author XinTu
* @classname SpringCloudConfigServerApp
* @description TODO
* @date 2023年03月23日 14:02
*/
@SpringBootApplication
@EnableConfigServer
public class SpringCloudConfigServerApp {
public static void main(String[] args) {
SpringApplication.run(SpringCloudConfigServerApp.class, args);
}
}
3.3 添加YML配置
新增配置文件 application.yml。配置文件添加内容如下,
# 服务端口
server:
port: 8086
#指定应用名称
spring:
application:
name: config-server
cloud:
config:
label: master #配置git仓库分支
server:
git:
uri: https://gitee.com/lwbook/spring-cloud-config.git #配置git仓库地址
search-paths: spring-cloud-config #配置仓库路径
#username: git_username #访问git仓库的用户名,公开仓库不配置用户名
#password: git_password #访问git仓库的用户密码,公开仓库不配置密码
3.4 创建远程分支及Profile配置文件
1) 远程配置仓库及文件根据自己公司环境自行创建。
这里老王提供一个自己创建的公共Git地址。https://gitee.com/lwbook/spring-cloud-config.git。
该仓库下新建了一个xintu-config文件夹。
在xintu-config 文件夹下分别创建3个文件。
注意:文件命名规格:{项目名}-{配置环境版本}.yml。比如application-dev.yml,表示的是application项目的开发环境配置。
📕开发环境配置: application-dev.yml
添加内容:
env: dev
test: 1
📕测试环境配置:application-test.yml
添加内容:
env: test
test: 2
📕预发环境配置:application-pre.yml
添加内容:
env: pre
test: 3
3.5 启动并测试服务
启动程序 SpringCloudConfigServerApp类后,输入http://localhost:8086/application/dev访问 springcloud-config-server服务。出现如下界面,则表示配置服务中心可以从远程程序获取配置信息。
{"name":"application","profiles":["dev"],"label":null,"version":"6abb9b9f47dedfe76592d3496a057dce4f74a9fe","state":null,"propertySources":[{"name":"https://gitee.com/lwbook/spring-cloud-config.git/xintu-config/application-dev.yml","source":{"env":"dev","test":1}}]}
看下后台日志打印,
至此,COnfig服务端配置完成。
4、客户端配置
创建一个新的module. 命名为:springcloud-config-client。
4.1 添加依赖
在springcloud-config-client pom.xml中添加config-client依赖。如下,
<!-- config client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<!-- 添加web 用于获取config数据测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.2 开启服务注册
新建启动类SpringCloudConfigClientApp, 代码示例如下,
package com.xintu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author XinTu
* @classname SpringCloudConfigClientApp
* @description TODO
* @date 2023年03月23日 14:02
*/
@SpringBootApplication
public class SpringCloudConfigClientApp {
public static void main(String[] args) {
SpringApplication.run(SpringCloudConfigClientApp.class, args);
}
}
新建一个测试controller类,在程序的启动类 ConfigClientController 通过 @Value 获取服务端的 env和 test值的内容。
代码示例:
package com.xintu.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author XinTu
* @classname ConfigClientController
* @description TODO
* @date 2023年03月23日 14:20
*/
@RestController
public class ConfigClientController {
@Value("${env}")
String env;
@Value("${test}")
String test;
@RequestMapping("/")
public String test() {
return String.format("env=%s;test=%s", env, test);
}
}
4.3 添加YML配置
添加配置文件 application.yml。
# 服务注册中心 (单节点)
server:
port: 8087
#指定应用名称
spring:
application:
name: config-client
cloud:
config:
label: master #配置git仓库分支
profile: dev #开发环境配置文件
uri: http://localhost:8086/ #指明配置服务中心的网址
#配置默认值,否则启动时会报错;配置中心有则用配置中心
env: prod
test: 4
4.4 启动并测试服务
启动程序 SpringCloudConfigClientApp类后,发现如下异常信息。
1)看第一条错误信息,加载的地址端口不是8086,而是8888.
2)看第二条错误信息,虽然端口没变,但是application、profile和lable变成了我们自己配置的内容。
我们再访问页面:http://localhost:8087/test
* 期望的是env=dev,test=3.而返回的是prod和4.说明我们的git配置没获取成功。
先说解决方案:
方案一:把application.yml换为bootstrap.yml文件即可。
方案二:把config serverz的端口改为8888,当然这种只限于localhost模式,跨服务器是行不通的。
问题原因分析见第5节。
5、原因分析
网上有类似的问题的回答,建议把application.yml调整为bootstrap.yml,修改后确实生效了。但这个是真的的答案吗?能解释问题1,但是无法解释问题2。所以,我们还需要继续查找问题根因。
先看官网:https://cloud.spring.io/spring-cloud-static/spring-cloud.html。
大致意思就是bootstrap的配置会优先application。但官网只解释了用bootstrap替换application的问题。但从前面的异常我们发现,虽然uri依然采用的是默认值,但是application、profile和lable这三个值被我们自己定义的覆盖了。很显然依然解释不清楚问题2。
难道是Spring Cloud的官网解释的有问题?随着老王的思路一步一步排查。
5.1 定位问题原因
找到打印异常信息的类,
c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:8888/. Will be trying the next url if available
Could not locate PropertySource: I/O error on GET request for "http://localhost:8888/application/dev/master": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect
发现时Spring Cloud Config自身类有一个uri默认值,也就是我们看到的ConfigClientProperties private String[] uri = { "http://localhost:8888" }。它使用了默认的值。
那可以推断出项目中application.yml没有把uri值覆盖掉,而且除application.yml外,其他地方也没有单独配置uri的值。但是name,lable和profile是生效了。所以这么看并不是application.yml没有生效啊?
5.2 定位问题2分析过程
那究竟时怎么覆盖的呢?我们可以从发送http请求出问题的地方入手。
调用的是org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#getRemoteEnvironment方法。那需要排查前面是否有单独处理三个值的地方。我们会发现有一个方法:
ConfigClientProperties properties = this.defaultProperties.override(environment); 这个地方有一个覆盖值的方法,进到方法内部。会发现这三个值被重新覆盖了。
那这就奇怪了,为什么要覆盖?被谁覆盖了?再解析属性的地方打断点,我们继续调试跟踪。
发现是从application.yml中获取的值。
那这就解释了为什么另外3个值会被覆盖的原因了。
5.3 定位问题1分析过程
解决了前面三个属性值覆盖问题,现在我们回头看uri的值为什么没被覆盖掉。
5.3.1 逆向排查
首先,我们需要看哪儿调用了这个类,发现这个类构造函数(仅有一个构造方法),
org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#ConfigServicePropertySourceLocator//构造函数。那此时我们就需要看谁调用了这个构造函数。
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration#configServicePropertySource这个方法。
继续回退,看是谁调用了configServicePropertySource这个方法。
结果没有地方调用。而且构造函数也没有被调用。那就看类在那些位置被加载了。
此时,我们注意到有一个spring.factories文件。这个回到了我们熟悉的spring类扫描机制。我们看到,这个类被spring cloud的四个文件所引用。先看第一个文件,spring.factories。
我们知道,有时候希望一些第三方包被Spring管理,但是不想被Spring Boot扫描到。通常我们会采用注解进行实例化,另外一种是使用spring.factories机制。那这里的spring.factories就是通过这种方式管理Spring Cloud的包的。继续跟踪代码spring.factories文件。
这个就要看,什么时候加载并注入的spring cloud类。那我们就要从头开始跟踪代码。
5.3.2 正向跟踪
1)定位监听
我们从main函数开始跟踪,进入到我们所熟悉的run这个方法。
org.springframework.boot.SpringApplication#run(java.lang.String...)
很关键的两行代码,
// 关键点1:扫描所有包下实现了ApplicationListener接口的实现类,提前注入依赖参数。
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(); //开启事件监听
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); //关键点2: 准备配置信息,这里是加载yml和properties的关键方法
}
此时,我们继续看BootstrapApplicationListener这个类是怎么样的处理机制?抓入口,看onApplicationEvent(ApplicationEnvironmentPreparedEvent event) 这个方法。这个是Spring Boot的上下文加载监听事件。
打断点继续跟踪。发现了一个很关键的信息,就是spring.config.name=bootstrap的信息。这个配置会影响后面如何加载配置文件的流程。
BootstrapApplicationListener加载完后,我们继续往后看。前面提到了两个关键点,第一个知道了其作用,那第二个是怎么触发的呢?继续看监听器列表,注意这个ConfigFileApplicationListener监听器,就是接下来加载配置文件(第二关键点的位置)。
2)定位配置
当执行到org.springframework.boot.context.config.ConfigFileApplicationListener#onApplicationEvent时,
此处时根据不同事件,执行不同的处理逻辑。
此处的event为ApplicationEnvironmentPreparedEvent,会进入到org.springframework.boot.context.config.ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent方法。
注意这句话,postProcessors.add(this); 表示将当前ConfigFileApplicationListener加到postProcess中。我们看org.springframework.boot.context.config.ConfigFileApplicationListener#postProcessEnvironment方法中调用了addPropertySources, 而addPropertySources中才是真正去调用加载配置文件的方法load()。
继续看load方法。
直接进入load的重载方法中,
这个方法就是我们前面让大家记住的spring.config.name=bootstrap, 这个方法会影响加载资源文件的先后顺序。
此处逻辑为,如果配置了spring.config.name,就执行自己配置的。
如果没有配置,则执行默认的,及application。
而在我们的BootstrapApplicationListener中,已经写死了spring.config.name=bootstrap,所以此时走名称为bootstrap的配置文件,继续跟踪。
我们debug也发现,这个名称就是bootstrap。也就是会扫描这几个路径下的boorstrap.yml或bootstrap.properties文件。
扫描路径,
综上,我们知道了,SpringCloud会先加载名为bootstrap的配置文件。
3)定位注入
前面我们知道了spring cloud会加载名为bootstrap的问题,那再什么位置给ConfigClientProperties进行赋值处理的呢?这个就回到了Spring注入类是,需要将相关依赖都注入进来。
所以在org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration注入时,会把所依赖的ConfigClientProperties类等也一起加载进来。
在调用栈中,我们看到,org.springframework.cloud.config.client.ConfigClientProperties是被当作参数初始化进来的。
综上,我们知道了Spring Cloud会优先加载bootstrap.yml或bootstrap.properties文件。那Spring Cloud为什么要这么做呢?
从config中以及参考官网的一些说法,我们能看到,bootstrap.yml可以理解成优先级别高的一些参数配置,不想被其他配置覆盖。而application是偏应用层面的,可能会因不同环境而不同。从Spring Cloud的设计者角度考虑,认为像git这种配置,应该是固定不变的。
以上!