目录
Spring Cloud Config是一个解决分布式系统的配置管理方案,同时Spring Cloud支持Git或Svn来存放配置文件,默认为Git。Spring Cloud Config分为服务端和客户端两个部分,其中服务端也被称为分布式配置中心。所以整体的流程是服务端从Git远程仓库中克隆配置文件并存到服务端,然后以接口的形式将配置文件的内容提供出去,客户端通过接口获取数据,然后依据此数据初始化自己的应用。
本文中Spring Cloud Config会被配置成服务化、配置数据动态刷新和请求失败重试的功能。服务化可以使服务端和客户端耦合度降低,动态刷新可以在不重启客户端的情况下,动态刷新配置数据,而请求失败重试可以保证在网络情况不好的情况下的服务可用性。其中Spring Cloud Config会整合Spring Cloud Bus,以此来实现服务端发送更新请求到Spring Cloud Bus,其收到消息后再自动通知所有客户端的效果,减少更新成本。本文中使用RabbitMQ消息代理作为通知通道。
笔者使用的Java版本是jdk-8u201,IDE使用的是IntelliJ IDEA 2019.2 x64,Spring Boot的版本是2.1.7.RELEASE,Spring Cloud的版本是Greenwich.SR2。同时本文所使用的项目代码沿用笔者之前写过的文章《Spring Cloud服务治理:Eureka+OpenFeign》中的项目代码,并在此基础上进行继续开发。
1 Git仓库
首先需要在GitHub上创建一个远程仓库,命名为testConfig(创建过程很简单,不会的可自行在网上查看,这里不再赘述)。然后需要创建一个本地仓库:本地创建一个文件夹,命名为testConfig,其中再创建一个文件夹,命名为client1。client1中存放着三个配置文件,分别为client1-dev.properties、client1-prod.properties和client1-test.properties,用来演示开发、生产和测试环境的配置文件。这三个文件中都只有一个相同的配置项,如下所示:
client1-dev.properties:
my.config=dev
client1-prod.properties:
my.config=prod
client1-test.properties:
my.config=test
接着打开Git Bash,将当前路径切换到testConfig的目录下,然后输入下面的命令,将本地的配置文件上传到刚刚配好的GitHub远程仓库中:
git init
git add .
git commit -m "微服务配置"
git remote add origin https://github.com/fake/testConfig.git
git push -u origin master
其中第四条命令中的远程仓库地址是个假地址,读者可自行替换成自己的git仓库地址即可。
上传完毕后的效果如下所示:
2 配置服务端
首先创建一个Spring Boot项目,命名为config-server。
2.1 pom.xml
<?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.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hys</groupId>
<artifactId>config-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>config-server</name>
<description>Demo project for Spring Cloud</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中除了服务端的依赖之外,spring-cloud-starter-netflix-eureka-client依赖是为了与Eureka服务注册中心进行整合,以此来实现服务化。而spring-cloud-starter-bus-amqp依赖是为了启用Spring Cloud Bus。
2.2 application.properties
spring.application.name=config-server
server.port=7001
spring.cloud.config.server.git.uri=https://github.com/fake/testConfig.git
spring.cloud.config.server.git.search-paths=client1
spring.cloud.config.server.git.username=username
spring.cloud.config.server.git.password=password
eureka.client.service-url.defaultZone=http://localhost:1111/eureka,http://localhost:1112/eureka,http://localhost:1113/eureka
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
management.endpoints.web.exposure.include=bus-refresh
spring.cloud.config.server.git.uri配置项表示GitHub远程仓库的地址,数据将会从这里加载(这里是假地址,需要读者自行替换成自己的GitHub仓库地址);
spring.cloud.config.server.git.search-paths配置项表示仓库中配置文件的地址,是个相对位置;
spring.cloud.config.server.git.username和spring.cloud.config.server.git.password配置项是自己的GitHub的用户名和密码,如果远程仓库是公开的,那么可以不用填写用户名和密码,如果是私有的仓库则必须填写;
eureka.client.service-url.defaultZone配置项是Eureka服务注册中心的地址;
spring.rabbitmq.host、spring.rabbitmq.port、spring.rabbitmq.username、spring.rabbitmq.password配置项是RabbitMQ的一些连接配置,这里用来实现Spring Cloud Bus的消息代理;
management.endpoints.web.exposure.include配置项在这里是用来暴露/actuator/bus-refresh接口,请求该接口会使服务端向Spring Cloud Bus发送请求,Spring Cloud Bus接收到请求后再向所有的客户端更新配置信息。
2.3 启动类
package com.hys.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
其中@EnableConfigServer注解用来开启Spring Cloud Config的服务端功能。
2.4 运行及结果
我们可以先单独启动服务端来查看结果。确保之前搭建的eureka-server项目处于运行状态,然后在Postman中访问http://localhost:7001/client1/dev/master,结果如下所示:
可以看到返回了我们在client1-dev.properties中的配置项内容,此时我们修改本地的client1-dev.properties内容:
my.config=dev123
将原来的dev改为dev123,再提交到GitHub上去,确保远程仓库中的内容发生了改变,这时我们再次访问http://localhost:7001/client1/dev/master,查看结果:
可以看到,配置项的值已经改为了dev123,由此可见服务端是能够实现配置内容动态刷新的效果。
3 配置客户端
首先创建一个Spring Boot项目,命名为config-client。
3.1 pom.xml
<?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.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hys</groupId>
<artifactId>config-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>config-client</name>
<description>Demo project for Spring Cloud</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中除了客户端的依赖之外,spring-boot-starter-web依赖是用来开启Spring MVC及Web支持的,spring-cloud-starter-netflix-eureka-client依赖是为了与Eureka服务注册中心进行整合,以此来实现服务化。spring-retry和spring-boot-starter-aop依赖则是用来支持请求失败重试的功能。spring-cloud-starter-bus-amqp依赖是为了启用Spring Cloud Bus。
3.2 bootstrap.properties
客户端的配置文件是放在bootstrap.properties而不是application.properties中,在resources目录下手工创建一个即可。bootstrap.properties文件的加载时机更早,优先级更高。
spring.application.name=client1
server.port=7002
spring.cloud.config.profile=dev
spring.cloud.config.label=master
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.discovery.enabled=true
eureka.client.service-url.defaultZone=http://localhost:1111/eureka,http://localhost:1112/eureka,http://localhost:1113/eureka
spring.cloud.config.fail-fast=true
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
eureka.instance.instance-id=${spring.application.name}:${server.port}
spring.application.name配置项的值不能随便写,其和{application}-{profile}.properties配置文件中的{application}一致。我们这里是client1-dev.properties,所以该值必须为client1;
spring.cloud.config.profile配置项的值和{application}-{profile}.properties配置文件中的{profile}一致,表示应用的环境;
spring.cloud.config.label配置项对应于{label},表示Git上的分支;
pring.cloud.config.discovery.service-id配置项表示配置Spring Cloud Config服务端的ServiceId,客户端通过这个值在Eureka注册中心中查找服务端的信息;
spring.cloud.config.discovery.enabled配置项为true表示开启通过Eureka获取Spring Cloud Config服务端的功能;
eureka.client.service-url.defaultZone配置项是Eureka服务注册中心的地址;
spring.cloud.config.fail-fast配置项为true表示开启请求失败重试;
spring.rabbitmq.host、spring.rabbitmq.port、spring.rabbitmq.username、spring.rabbitmq.password配置项是RabbitMQ的一些连接配置,这里用来实现Spring Cloud Bus的消息代理;
eureka.instance.instance-id配置项是为了给每一个客户端一个实例id,是由服务名:端口号组成。这里是用来模拟Spring Cloud Bus逐个刷新的效果。
3.3 启动类
package com.hys.configclient;
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);
}
}
3.4 Controller
package com.hys.configclient.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope
@RestController
public class HelloController {
@Value("${my.config}")
private String hello;
@GetMapping("/hello")
public String hello() {
return hello;
}
}
hello方法读取当前配置文件中my.config的值并返回。
3.5 运行及结果
保证之前的eureka-server和config-server项目处于运行状态,同时保证RabbitMQ处于运行中。将本项目打包后,分别执行下面的命令来启动两个客户端:
java -jar config-client-0.0.1-SNAPSHOT.jar
java -jar config-client-0.0.1-SNAPSHOT.jar --server.port=7003
启动完成后,在Eureka的注册中心中查看得到以下的结果:
一个服务端,两个客户端。首先我们在页面中访问http://localhost:7002/hello和http://localhost:7003/hello,结果如下:
由上可知,之前我们在服务端的演示中,将该配置项的值改为了dev123。现在我们将client1-dev.properties中的内容再做些修改:
my.config=dev
现在又改回了dev。然后将改变上传到GitHub远程仓库中。在Postman中访问http://localhost:7001/actuator/bus-refresh,注意请求是Post请求:
我们向服务端发送了/actuator/bus-refresh请求,该请求会发送到Spring Cloud Bus中,并最终更新到所有的客户端。这时我们再在页面中访问http://localhost:7002/hello和http://localhost:7003/hello,结果如下:
可以看到,两个客户端的配置都已经改为了dev,客户端的动态刷新是成功的。如果我们不用Spring Cloud Bus的话,那么就得在每个客户端中都请求一次/refresh接口,如果客户端很多的话,维护起来负担也是非常大。所以这就是Spring Cloud Bus的意义,只需要向服务端请求一次刷新接口,Spring Cloud Bus就能将所有客户端都进行更新。
同时我们可以在页面上访问http://localhost:15672,查看RabbitMQ的Web管理界面,查看到Spring Cloud Bus已经创建了一个名为springCloudBus的交换器以及一些相关的队列:
除了支持通知所有的客户端之外,Spring Cloud Bus还支持逐个客户端的通知更新操作。只需要向服务端输入如下的访问http://localhost:7001/actuator/bus-refresh/client1:7003即可:
以上操作的效果是只对7003端口的客户端做更新通知,而7002端口的客户端不会收到通知。在输入上述url之前我们先将本地配置文件中该项的值再次改为dev123,然后上传到GitHub。接着再输入上面只对7003端口的客户端进行刷新通知的url。最后在页面中访问http://localhost:7002/hello和http://localhost:7003/hello,以验证结果:
由上面可以看到,正如我们预想的那样,只有7003端口的客户端成功刷新了配置,7002端口的客户端没有刷新配置,验证成功。
最后来验证请求失败重试的效果。如果没有请求失败重试并且请求失败的原因只是因为网络波动等其他间歇性原因导致的,那么直接启动失败似乎代价有些高,所以从这种角度来说请求失败重试还是很有意义的。我们现在将服务端和客户端都停掉,只启动客户端代码,以此来模拟请求失败的情况。查看后台打印的日志:
2019-08-13 06:07:55.156 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:07:57.195 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:07:58.197 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:08:00.213 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:08:01.315 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:08:03.331 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:08:04.543 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:08:06.561 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:08:07.895 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:08:09.912 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:08:11.378 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:7001/
2019-08-13 06:08:13.395 INFO 16680 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Connect Timeout Exception on Url - http://localhost:7001/. Will be trying the next url if available
2019-08-13 06:08:13.401 ERROR 16680 --- [ main] o.s.boot.SpringApplication : Application run failed
其中可以看到有如上所示的内容,请求一共发送了6次,重试了5次,这是默认的策略,可以自行更改。请求失败重试可以避免一些间歇性问题引起的失败导致客户端应用无法启动的情况出现。