Spring Cloud: 配置中心(Git 版与动态刷新)

文章目录

  • 准备工作
  • Server 端
  • Client 端
  • Refresh
  • Webhook
  • 问题

开发版本为 Spring Boot 2.2.4.RELEASE 版本和 Spring Cloud Hoxton.SR1 版本,开发工具为 Eclipse IDE for Enterprise Java Developers(Version: 2019-09 R (4.13.0)),Jave 版本为 1.8。

本文为 https://windmt.com/2018/04/19/spring-cloud-7-config-sample/ 的学习笔记。

Spring Cloud Config 是 Spring Cloud 团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密 / 解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config 实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于 Spring 构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于 Spring Cloud Config 实现的配置中心默认采用 Git 来存储配置信息,所以使用 Spring Cloud Config 构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过 Git 客户端工具来方便的管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如:SVN 仓库、本地化文件系统。

在本文中,我们将学习如何构建一个基于 Git 存储的分布式配置中心,并对客户端进行改造,并让其能够从配置中心获取配置信息并绑定到代码中的整个过程。最后,我们还将了解如何能让客户端获取到修改后的最新配置。

准备工作

准备一个 Git 仓库,在 Github 上面创建了一个文件夹 config-repo 用来存放配置文件,为了模拟生产环境,我们创建以下三个配置文件:

// 开发环境
config-client-dev.yml
// 测试环境
config-client-test.yml
// 生产环境
config-client-prod.yml

每个配置文件中都写一个属性 info.profile, 属性值分别是 dev/test/prod。下面我们开始配置 Server 端。
Spring Cloud: 配置中心(Git 版与动态刷新)_第1张图片

info:
  profile: dev

Server 端

  1. 创建一个 Spring boot 工程
    打开 File -> New -> Project,选择 Spring Starter Project,点击 Next。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第2张图片
    name 设置为 config-server-git,点击 Next。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第3张图片
    选择 Config Server,点击 Finish。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第4张图片
  2. 自动生成的 pom.xml 文件内容如下:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
	<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		<version>2.2.5.RELEASEversion>
		<relativePath/> 
	parent>
	<groupId>com.examplegroupId>
	<artifactId>config-server-gitartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>config-server-gitname>
	<description>Demo project for Spring Bootdescription>

	<properties>
		<java.version>1.8java.version>
		<spring-cloud.version>Hoxton.SR3spring-cloud.version>
	properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-config-serverartifactId>
		dependency>

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintagegroupId>
					<artifactId>junit-vintage-engineartifactId>
				exclusion>
			exclusions>
		dependency>
	dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloudgroupId>
				<artifactId>spring-cloud-dependenciesartifactId>
				<version>${spring-cloud.version}version>
				<type>pomtype>
				<scope>importscope>
			dependency>
		dependencies>
	dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

其中比较重要的配置为:

<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-config-serverartifactId>
dependency>
  1. 在 resource 文件夹下添加 application.yml 配置文件
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/iammodest/spring-cloud-study # 配置git仓库的地址
          search-paths: config-repo # git仓库地址下的相对地址,可以配置多个,用,分割。
server:
  port: 12000

Spring Cloud Config 也提供本地存储配置的方式。我们只需要设置属性 spring.profiles.active=native,Config Server 会默认从应用的 src/main/resource 目录下检索配置文件。也可以通过 spring.cloud.config.server.native.searchLocations=file:E:/properties/ 属性来指定配置文件的位置。虽然 Spring Cloud Config 提供了这样的功能,但是为了支持更好的管理内容和版本控制的功能,还是推荐使用 Git 的方式。

如果我们的 Git 仓库需要权限访问,那么可以通过配置下面的两个属性来实现;
spring.cloud.config.server.git.username:访问 Git 仓库的用户名
spring.cloud.config.server.git.password:访问 Git 仓库的用户密码

  1. 启动类添加 @EnableConfigServer,激活对配置中心的支持。
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerGitApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigServerGitApplication.class, args);
	}

}
  1. 测试
    首先我们先要测试 Server 端是否可以读取到 github 上面的配置信息,直接访问 http://localhost:12000/config-client/dev 返回信息如下:
{
	"name": "config-client",
	"profiles": ["dev"],
	"label": null,
	"version": "6ebbf7080501376655600a9445702df5715cce43",
	"state": null,
	"propertySources": [{
		"name": "https://github.com/iammodest/spring-cloud-study/config-repo/config-client-dev.yml",
		"source": {
			"info.profile": "dev"
		}
	}]
}

上述的返回的信息包含了配置文件的位置、版本、配置文件的名称以及配置文件中的具体内容,说明 Server 端已经成功获取了 Git 仓库的配置信息。

如果直接查看配置文件中的配置信息可访问 http://localhost:12000/config-client-dev.yml 返回:

info:
  profile: dev

修改配置文件 config-client-dev.yml 中配置信息为:dev update, 再次在浏览器访问 http://localhost:12000/config-client-dev.yml 返回:dev update,说明 Server 端会自动读取最新提交的内容。

Client 端

在完成了上述验证之后,确定配置服务中心已经正常运作,下面我们尝试如何在微服务应用中获取上述的配置信息。
再创建一个基础的 Spring Boot 应用,命名为 config-client

  1. 创建一个 Spring boot 工程
    打开 File -> New -> Project,选择 Spring Starter Project,点击 Next。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第5张图片
    name 设置为 config-client,点击 Next。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第6张图片
    选择 Config ClientSpring Reactive Web,点击 Finish。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第7张图片
  2. 自动生成的 pom.xml 文件内容如下:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
	<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		<version>2.2.5.RELEASEversion>
		<relativePath/> 
	parent>
	<groupId>com.examplegroupId>
	<artifactId>config-client-1artifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>config-client-1name>
	<description>Demo project for Spring Bootdescription>

	<properties>
		<java.version>1.8java.version>
		<spring-cloud.version>Hoxton.SR3spring-cloud.version>
	properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webfluxartifactId>
		dependency>
		
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-starter-configartifactId>
		dependency>

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintagegroupId>
					<artifactId>junit-vintage-engineartifactId>
				exclusion>
			exclusions>
		dependency>
		<dependency>
			<groupId>io.projectreactorgroupId>
			<artifactId>reactor-testartifactId>
			<scope>testscope>
		dependency>
	dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloudgroupId>
				<artifactId>spring-cloud-dependenciesartifactId>
				<version>${spring-cloud.version}version>
				<type>pomtype>
				<scope>importscope>
			dependency>
		dependencies>
	dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

其中比较重要的配置为:

<dependency>
	<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webfluxartifactId>
dependency>
	
<dependency>
	<groupId>org.springframework.cloudgroupId>
	<artifactId>spring-cloud-starter-configartifactId>
dependency>
  1. 在 resource 文件夹下添加 application.ymlbootstrap.yml 配置文件

application.yml

spring:
  application:
    name: config-git
server:
  port: 13000

bootstrap.yml

spring:
  cloud:
    config:
      uri: http://localhost:12000 # 配置中心的具体地址,即 config-server
      name: config-client # 对应 {application} 部分
      profile: dev # 对应 {profile} 部分
      label: master # 对应 {label} 部分,即 Git 的分支。如果配置中心使用的是本地存储,则该参数无用

特别注意:上面这些与 Spring Cloud Config 相关的属性必须配置在 bootstrap.yml 中,config 部分内容才能被正确加载。因为 config 的相关配置会先于 application.yml,而 bootstrap.yml 的加载也是先于 application.yml。

  1. 启动类无需修改,只用 @SpringBootApplication 即可。
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ConfigClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigClientApplication.class, args);
	}

}
  1. 新建 package controller,新增类 HelloController
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import reactor.core.publisher.Mono;

@RestController
public class HelloController {
	@Value("${info.profile:error}")
	private String profile;

	@GetMapping("/info")
	public Mono<String> hello() {
		return Mono.justOrEmpty(profile);
	}
	
}
  1. 测试
    启动项目后访问 http://localhost:13000/info 返回 dev 说明已经正确的从 Server 端获取到了参数。到此一个完整的服务端提供配置服务,客户端获取配置参数的例子就完成了。

我们再做一个小实验,手动修改 config-client-dev.yml 中配置信息为:dev update 提交到 Github, 再次在浏览器访问 http://localhost:13000/info 返回:dev,说明获取的信息还是旧的参数,这是为什么呢?

因为 Spring Cloud Config 分服务端和客户端,服务端负责将 Git 中存储的配置文件发布成 REST 接口,客户端可以从服务端 REST 接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置。客户端如何去主动获取新的配置信息呢,Spring Cloud 已经给我们提供了解决方案,每个客户端通过 POST 方法触发各自的 /actuator/refresh。

Refresh

仅修改客户端即 config-client 项目,就可以实现 refresh 的功能。

  1. 添加依赖
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

增加了 spring-boot-starter-actuator 包,spring-boot-starter-actuator 是一套监控的功能,可以监控程序在运行时状态,其中就包括 /actuator/refresh 的功能。
2. 开启更新机制
需要给加载变量的类上面加载 @RefreshScope,在客户端执行 /actuator/refresh 的时候就会更新此类下面的变量值。

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import reactor.core.publisher.Mono;

@RestController
@RefreshScope
public class HelloController {
	@Value("${info.profile:error}")
	private String profile;

	@GetMapping("/info")
	public Mono<String> hello() {
		return Mono.justOrEmpty(profile);
	}
	
}

  1. 修改 application.yml 文件,使 /actuator/refresh 这个 Endpoint 暴露出来:
spring:
  application:
    name: config-git
server:
  port: 13000
management:
  endpoints:
    web:
      exposure:
        include: refresh  
  1. 测试
    改造完之后,我们重启 config-client,我们以 POST 请求的方式来访问 http://localhost:13000/actuator/refresh 就会更新配置文件至最新版本。

具体测试:
1. 访问 http://localhost:13000/info 返回 dev
2. 将 Git 上对应配置文件里的值改为 dev update
3. 打开 cmd,执行 curl -X POST http://localhost:13000/actuator/refresh,返回 ["config.client.version","info.profile"]
4. 再次访问 http://localhost:13000/info 返回 dev update

这就说明客户端已经得到了最新的值,Refresh 是有效的。

不过,每次手动刷新客户端也很麻烦,有没有什么办法只要提交代码就自动调用客户端来更新呢,Github 的 Webhook 是一个办法。

Webhook

Webhook 是当某个事件发生时,通过发送 HTTP POST 请求的方式来通知信息接收方。Webhook 来监测你在 Github.com 上的各种事件,最常见的莫过于 push 事件。如果你设置了一个监测 push 事件的 Webhook,那么每当你的这个项目有了任何提交,这个 Webhook 都会被触发,这时 Github 就会发送一个 HTTP POST 请求到你配置好的地址。

如此一来,你就可以通过这种方式去自动完成一些重复性工作,比如,你可以用 Webhook 来自动触发一些持续集成(CI)工具的运作,比如 Travis CI;又或者是通过 Webhook 去部署你的线上服务器。下图就是 Github 上面的 Webhook 配置。
Spring Cloud: 配置中心(Git 版与动态刷新)_第8张图片

  • Payload URL :触发后回调的 URL
  • Content type :数据格式,两种一般使用 json
  • Secret:用作给 POST 的 body 加密的字符串。采用 HMAC 算法
  • events :触发的事件列表。
event事件类型 描述
push 仓库有 push 时触发。默认事件
create 当有分支或标签被创建时触发
delete 当有分支或标签被删除时触发

这样我们就可以利用 hook 的机制去触发客户端的更新,但是当客户端越来越多的时候,hook 机制也不够优雅了,另外每次增加客户端都需要改动 hook 也是不现实的。其实,Spring Cloud 给了我们更好解决方案 ——Spring Cloud Bus。后续我们将继续学习如何通过 Spring Cloud Bus 来实现以消息总线的方式进行通知配置信息的变化,完成集群上的自动化更新。

问题

  1. 使用 Webhook 时,如果在本地测试,会发现外网是无法直接访问本地的,这里可以利用内网穿透使得外网可以访问本地,实现内网穿透有很多方式,这里我是用的花生壳,具体设置可参考资料。
  2. 在设置好内网穿透后,如下设置 webhook,Payload URL 设置为 http://301108jy76.zicp.vip:80/actuator/refresh(此时会自动跳转访问http://localhost:13000/actuator/refresh),Content type 设置为 application/json,选择 push 时自动触发。
    Spring Cloud: 配置中心(Git 版与动态刷新)_第9张图片
    但是测试时,查看 Recent Delivery,发现以下错误:
{"timestamp":"2020-03-14T17:02:54.831+0000","path":"/actuator/refresh","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"1388bdc5-18"}

Spring Cloud: 配置中心(Git 版与动态刷新)_第10张图片
尝试在 postman 中测试调用,也是同样错误:
Spring Cloud: 配置中心(Git 版与动态刷新)_第11张图片
经过多次测试,发现是因为 github 在触发 webhook 发送 post 请求时,会发送很长一段 body 数据,在我尝试删减 body 后发现,只要 body 数据含有两层就会出现错误,如下:

{
    "ref": "refs/heads/master",
    "before": "ef7d6ffc65f75527c5dc21f09152e4f80bb94e71",
    "after": "2d3d3176a26fd60df4b15992a8d681c465103ff0",
    "repository": {
        "id": 247197927
    }
}

而再次删减后,则可正常结束:

{
    "ref": "refs/heads/master",
    "before": "ef7d6ffc65f75527c5dc21f09152e4f80bb94e71",
    "after": "2d3d3176a26fd60df4b15992a8d681c465103ff0"
}

Spring Cloud: 配置中心(Git 版与动态刷新)_第12张图片
推测可能 actuator/refresh 节点解析 body 数据时,如果 json 有多层结果,会出现错误,所以打算再定义一个 POST 服务,用来过滤 body 数据后,再中转申请 http://localhost:13000/actuator/refresh 请求,实现自动更新。
具体实现:

  1. 创建类 UtilController:
package com.example.demo.controller;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UtilController {
	
	@RequestMapping("/refresh")
	public String refresh() {		
		RestTemplate restTemplate = new RestTemplate();
		HttpHeaders httpHeaders = new HttpHeaders();
		httpHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
		HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(null, httpHeaders);
		
		ResponseEntity<String> stringResponseEntity = restTemplate
				.postForEntity("http://localhost:13000/actuator/refresh", request, String.class);
		
		return stringResponseEntity.getBody();
	}
}

这里定义了一个 POST 方式的服务 refresh,然后再自动 POST 访问 http://localhost:13000/actuator/refresh,这里我写死了,host 和 端口 可以动态获取后拼接。重启项目后,POST 访问 http://localhost:13000/refresh,可正常刷新。
一开始我是将 refresh 服务定义在 HelloController 内的,但是这会卡住,具体单步调试后,发现是有一句 lock 语句卡住了,怀疑在 @RefreshScope 范围内是不允许触发 actuator/refresh 的,这个不深究了。

你可能感兴趣的:(Spring)