spring config从github同步配置利用rabbitmq热更新微服务配置(踩坑)

Spring Cloud Config (前言)

引用Spring cloud官方文档中的话

Spring Cloud Config为分布式系统中的外部化配置提供服务器端和客户端支持。使用Config Server,您可以在所有环境中管理应用程序的外部属性。客户端和服务器上的概念映射与Spring Environment和PropertySource抽象,因此它们非常适合Spring应用程序,但可以与任何语言运行的任何应用程序一起使用。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配置,并确保应用程序具有迁移时需要运行的所有内容。服务器存储后端的默认实现使用git,因此它可以轻松支持配置环境的标签版本,以及可用于管理内容的各种工具。添加替代实现并使用Spring配置插入它们很容易。

简单来说Spring Cloud Config 是微服务中用来外部管理应用配置文件的一种解决方案.

我项目中使用到的Spring版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.BUILD-SNAPSHOT</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.BUILD-SNAPSHOT</spring-cloud.version>
</properties>

Spring Cloud Config Server

为了实现微服务应用配置的外部管理,我们首先肯定需要一个管理这些配置的一个微服务,而spring config server 就是这样的一个微服务

  • 在config server项目的pom文件中添加依赖
<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>
  • application.yml的配置(这里的git需要自己配置,我配置的是github)
spring:
  application:
    name: config
  cloud:
    config:
      server:
        git:
          uri: 远程git仓库的地址
          username: git用户名
          password: git密码
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

server:
  port: 8078
management:
  endpoints:
    web:
      exposure:
        include: "*"

Spring Cloud Config Client

之后就是创建需要被管理配置文件的项目了,这里我创建的项目名字叫order项目spring版本与之前一样

  • pom文件的依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

那么这个项目就不需要application.yml了,但是我们需要向config server请求配置文件,如果没有application.yml那么spring知道像哪里请求吗,当然不知道。所以我们还是需要配置,只是这次配置的文件是bootstrap.yml这个文件在spring启动时立刻被加载,优先远远高于application.yml。

spring:
  application:
    name: order
  cloud:
    config:
      discovery:
        service-id: CONFIG
        enabled: true
      profile: dev

其中我们需要指出自己的应用名,以及需要请求的应用的应用名,以及请求的配置文件后缀.
完成了之后就可以测试了,当然我们使用的bus依赖的后缀是amqp所以我们还需要在系统中启动rabbitmq,当然bus也支持kafka。那么这里我们启动了rabbitmq,用的是默认端口。
在git上创建order-dev.yml文件
github项目中的文件
并且在其中添加内容

spring:
  application:
    name: order
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/scloud?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  jpa:
    show-sql: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
server:
  port: 8085

完成之后启动eureka,启动config server 再启动config client 可以发现日志输出获取成功信息

2019-03-27 16:09:17.088  INFO 9660 --- [sODVEsF8DPtbg-1] com.netflix.discovery.DiscoveryClient    : Getting all instance registry info from the eureka server
2019-03-27 16:09:17.110  INFO 9660 --- [sODVEsF8DPtbg-1] com.netflix.discovery.DiscoveryClient    : The response status is 200
2019-03-27 16:09:17.111  INFO 9660 --- [sODVEsF8DPtbg-1] com.netflix.discovery.DiscoveryClient    : Not registering with Eureka server per configuration
2019-03-27 16:09:17.111  INFO 9660 --- [sODVEsF8DPtbg-1] com.netflix.discovery.DiscoveryClient    : Discovery Client initialized at timestamp 1553674157111 with initial instances count: 2
2019-03-27 16:09:17.368  INFO 9660 --- [sODVEsF8DPtbg-1] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://SKY-20171010GGE:8078/
2019-03-27 16:09:23.339  INFO 9660 --- [sODVEsF8DPtbg-1] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=order, profiles=[dev], label=null, version=8d96131121f32078e31e27ba5b2774a0065c9eb1, state=null

可以发现Fetching config from server at : http://SKY-20171010GGE:8078/我们已经从云上获取到了配置文件.
但是我们的需求是能够在云上修改配置文件,并且不用重启项目热更新配置文件。明显上面只是完成了最基础的第一步。

热更新项目配置文件

这时候我们在github上修改我们的配置文件内容,然后再不重启项目的情况下会发现本地的配置并不能更新,还是原来的内容
spring config server为我们提供了这样的一个接口,而且我们在server端的application.yml中也通过配置暴露了这些端口.

management:
  endpoints:
    web:
      exposure:
        include: "*"

我们启动config server时日志也帮我们输出了这些端口映射

2019-03-27 15:37:29.043  INFO 11832 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/bus-env],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2019-03-27 15:37:29.043  INFO 11832 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/bus-env/{destination}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2019-03-27 15:37:29.043  INFO 11832 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/bus-refresh/{destination}],methods=[POST]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2019-03-27 15:37:29.043  INFO 11832 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/bus-refresh],methods=[POST]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)

我们要用的就是这个/actuator/bus-refresh它是一个post接口,那么我们利用postman发送这个请求
利用postman发送请求
会发现config server 与 order 项目的控制面板中会输出日志,并且配置文件也成功更新了(这里验证配置文件是否更新可以写一个contorller,在其中注入配置文件参数,使用浏览器访问去查看配置文件是否刷新.注意:必须在要刷新的类上加入@RefreshScope注解)
exp:

@RestController
@RequestMapping("/env")
@RefreshScope
public class EnvController {

    @Value("${env}")
    private String env;

    @GetMapping("print")
    public String print(){
        return env;
    }
}

但是每次需要我们手动发送post请求的方式过于繁琐,而且不够智能。所以此时我们需要利用github上提供的webhook功能(一般的git都会提供webhook功能)次功能可以实现git上的文件在被创建,修改,删除时自动发送post请求给指定的config server。

WebHook & NATAPP

我以github的webhook为例
在设置中找到webhook

  • 新建一个webhook
    在这里插入图片描述
    明显我们需要指定一个让github发送请求的一个外网地址,但是我们在内网测试时明显没有外网地址,这里我们使用一个NATAPP小程序 https://natapp.cn/

  • 1.进入网站注册账号
    NATAPP官网

  • 2.点击购买隧道选择免费隧道
    购买隧道界面
    点击购买
    点击购买

  • 3.下载客户端
    这里我用的是windows所以下windows客户端
    下载客户端

  • 4.启动客户端
    首先找到自己客户端的authtoken
    authtoken
    在客户端根目录下打开cmdnatapp -authtoken=9ab6b9040a624f40等号后面的token值是你们自己的token值
    输入完成
    之后我们就可以获得一个外网地址,这里我把这个外网地址映射到了我自己电脑上的8078端口上。也就是我config server的端口。
    那么我们就可以把这个外网的地址交给github了(注意,这个是免费隧道,每次重启都会换外网地址,所以仅供测试使用)
    在这里插入图片描述
    我们让github发送一个json请求到我们服务器。
    此时我们修改一下github上的配置文件,来做一下测试。
    发现NATAPP控制台输出400
    NATAPP console

2019-03-28 09:28:39.584  WARN 9288 --- [nio-8078-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token
 at [Source: (PushbackInputStream); line: 1, column: 293] (through reference chain: java.util.LinkedHashMap["commits"])

idea的log显示无法解析json。
坑1:我看的视频教程中无需手动引入monitor依赖(我和教程中的spring版本一致)
这里我们查看spring config官方文档
Spring Cloud Config 文档
发现官方提示,如果要使用github gitlab之类的webhook会发送POST请求以及携带JSON body,我们需要添加spring-cloud-config-monitor的依赖,然后让github访问/monitor接口。
/monitor
此时会发现config server的控制台中输出日志,成功从github中更新配置文件到本地。但是!此时我们会发现order项目中并没有与预期一样输出日志更新配置文件,访问之前测试的env/print端口发现还是旧的配置文件。(这里就是我踩坑的内容了)
因为之前的内容和教程上还是差不多一样,并没有出现太大问题,而我的版本也和教程上的一样,但是教程上显示order项目也同时更新了配置文件,但是我本地确实没有更新,此时我决定去查看一下monitor的源码
monitor源码
展开我们依赖的jar包发现其中有一个GitHub的一个类。

@Order(Ordered.LOWEST_PRECEDENCE - 300)
public class GithubPropertyPathNotificationExtractor
		implements PropertyPathNotificationExtractor {

	@Override
	public PropertyPathNotification extract(MultiValueMap<String, String> headers,
			Map<String, Object> request) {
		if ("push".equals(headers.getFirst("X-Github-Event"))) {
			if (request.get("commits") instanceof Collection) {
				Set<String> paths = new HashSet<>();
				@SuppressWarnings("unchecked")
				Collection<Map<String, Object>> commits = (Collection<Map<String, Object>>) request
						.get("commits");
				for (Map<String, Object> commit : commits) {
					addAllPaths(paths, commit, "added");
					addAllPaths(paths, commit, "removed");
					addAllPaths(paths, commit, "modified");
				}
				if (!paths.isEmpty()) {
					return new PropertyPathNotification(paths.toArray(new String[0]));
				}
			}
		}
		return null;
	}

	private void addAllPaths(Set<String> paths, Map<String, Object> commit, String name) {
		@SuppressWarnings("unchecked")
		Collection<String> files = (Collection<String>) commit.get(name);
		if (files != null) {
			paths.addAll(files);
		}
	}

}

于是我在其中打了断点,一步一步追踪下去(省略中间过程)
最终在org.springframework.cloud.bus.event.RemoteApplicationEvent类中发现

protected RemoteApplicationEvent(Object source, String originService,
			String destinationService) {
		super(source);
		this.originService = originService;
		if (destinationService == null) {
			destinationService = "**";
		}
		// If the destinationService is not already a wildcard, match everything that follows
		// if there at most two path elements, and last element is not a global wildcard already
		if (!"**".equals(destinationService)) {
			if (StringUtils.countOccurrencesOf(destinationService, ":") <= 1
					&& !StringUtils.endsWithIgnoreCase(destinationService, ":**")) {
				// All instances of the destination unless specifically requested
				destinationService = destinationService + ":**";
			}
		}
		this.destinationService = destinationService;
		this.id = UUID.randomUUID().toString();
	}

发现他会给指定的client发布一个消息,让他们去reload
指定client
指定client
发现这里指定的client名字都是order:dev 或者order-dev,但是我们的应用名叫order,是不是这个原因,于是我手动去github上新建了一个名叫order.yml的配置文件,在点击保存时,发现order项目成功热更新配置文件。原来是应用名字的原因导致order服务没有被成功接收到消息。
解决方案:
这里的解决方案有2种,请自行选择。(我个人用的是第二种方法)

  • 1.github上的配置文件名字改为order,当然这时修改的话,已order命名的左右client都会收到消息reload
  • 2.另一种就是把自己的这个服务的name修改为order-dev这样的话就能够接受到reload的请求了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值