SpringBoot 项目通常将一些重要的参数配置在application.yml或者application.properites中,譬如外部服务连接、数据库地址及账号信息、某些业务变量。随着业务的开展,实现一个完整的业务流程通常需要开发并管理多个微服务,一旦这些参数调整,需要修改多个微服务的配置并重启,这将给运维及生产带来额外的工作和影响。为了解决这个问题,我们可以将这些配置抽取到一个公共的地方,可参照另一篇文章“Spring Cloud 微服务公共配置处理请添加链接描述”,将微服务的公共配置抽取出来。完成抽取只是第一步,一旦数据库的账号或者外部服务连接修改,服务还是需要逐个重启,如何解决这个问题呢?

Spring Cloud的message和integration机制,允许我们通过cloud bus,将变更通知到每一个服务,实现动态刷新,其架构设计如下(摘自网络)

实现这个过程,需要引入spring cloud的相关组件:注册服务、消息服务、集成服务、配置服务等,以及一个可用的消息队列。下面我将以Springboot2.1.8,cloud版本Greenwich.SR3为例,介绍如何实现。

服务清单

  • EurekaServer - 注册服务,实际项目可选其他,此处仅作示例。
  • ConfigServer - 配置服务
  • ConfigClient1 - 微服务应用1,通过config server获取配置
  • ConfigClient2 - 微服务应用2,通过config server获取配置
  • RabbitMQ - 消息服务,用来接收和转发 bus消息
  • Git服务 - 配置文件的存储和修改,实际项目中可选其他替代方案,此处仅作示例。

1. EurekaServer

Eureka服务比较简单,配置和启动效果如下。

server:
  port: 7777
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      default-zone: http://${eureka.instance.hostname}:${server.port}/eureka/

2. ConfigServer

Configserver需要引入bus-amqp,pom配置如下:

  1. boot相关
	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-autoconfigure</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
  1. cloud相关
<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-context</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-commons</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-bus</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-bus-amqp</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-monitor</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-netflix-eureka-client</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-stream</artifactId>
		</dependency>

application的配置如下,

  1. spring config相关:

  2. management相关:

3. ConfigClient01 和ConfigClient02

  1. bootstrap.yml配置

management的配置是为了支持bus-fresh,rabbitmq需要同时配置在bootstrap和application中。

  1. application配置

主要包括eureka配置,rabbitmq配置及用来测试ext.key。configclient02的配置和01几乎一致,仅端口和应用名称不同。

所以服务启动后,可以看到eureka的管理界面如下:

4. git仓库中配置服务application文件的替代版

内容分别如下:

ext.key=hello client 111

ext.key=hello client 222

这个时候就可以测试了。测试方式可以通过浏览器访问configserver,直接请求配置文件,也可以通过程序测试,先说通过浏览器访问。

springboot项目启动时,如果引入了config-client和context包,则会自动根据bootstrap指定的地址获取配置,请求方式如下: http://${spring.cloud.config.uri}/${spring.cloud.config.name}/<spring.cloud.config.profile>/<lspring.cloud.config.label>, 参照bootstrap.yml的定义。

这个项目直接请求:http://localhost:8021/cfg_client01/dev/aliyun_dev, 返回如下: 这种请求方式,也是springboot项目启动时初始化的请求方式。

下面以更实际的方式测试,在controller中引用一个变量,ext.key, 查看属性的变化。

@RestController
@RequestMapping("/test")
@RefreshScope
public class IndexController {

    @Value("${ext.key}")
    private String key;

    @GetMapping("/key")
    public String printKey() {
        return key;
    }
}

controller很简单,唯一需要注意的是,一定要加上@RefreshScope,因为spring在刷新时,会基于这个注解,确定要不要刷新bean或者方法内的引用属性。 第一次直接请求:

现在修改git上的配置文件,将ext.key改成111000和222000。

这个时候就可以调用spring的bus-refresh了。调用refresh的时候,可以直接调用server的,也可以调用某个具体的client,我希望一次刷新,所有的变更都生效,所以直接调用configserver的bus-refresh。当然,也有人将git的commit配置成hookurl,但这种方式在生产环境会有些风险,我们的做法是,配置文件修改并确认提交后,手动执行一次bus-refrehs。

刷新完成后,分别在请求extkey,查看返回:

可以看到两个应用服务的属性都已经刷新。实现刷新的关键有以下几个:

  • 消息队列的在bootstrap和applicaiton中同时配置
  • management.endpoints.web.exposure.include属性需要配成"*"或者 bus-refresh,因为这个地址默认不支持,直接调用会出现404.
  • 验证服务,网上很多介绍都需要关闭,我测试过程中,没有关闭,但是可以直接访问。实践者可以根据自己的服务配置调整。

基于此,我们就实践了配置文件中属性的动态刷新,实现一次刷新,所有服务生效。这种@Value引用的属性可以这么刷新,那么数据库连接参数呢?下一篇文章,我将介绍如何通过刷新机制,实现在不重启服务的前提下,动态修改数据库的连接参数或者密码。