您现在的位置是:首页 >技术杂谈 >Spring Cloud Config配置服务及那些你不知道的坑网站首页技术杂谈

Spring Cloud Config配置服务及那些你不知道的坑

老王随聊 2023-05-21 20:00:02
简介Spring Cloud Config配置服务及那些你不知道的坑

目录

1、为什么选择Spring Cloud Config

1.1 集中式管理

1.2 动态修改配置

2、Spring Cloud Config 简介

3、服务端配置

3.1 添加依赖

3.2 开启服务注册

3.3 添加YML配置

3.4 创建远程分支及Profile配置文件

3.5 启动并测试服务

4、客户端配置

4.1 添加依赖

4.2 开启服务注册

4.3 添加YML配置

4.4 启动并测试服务

5、原因分析

5.1 定位问题原因

5.2 定位问题2分析过程

5.3 定位问题1分析过程

5.3.1 逆向排查

5.3.2 正向跟踪


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这种配置,应该是固定不变的。

以上!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。