引用Spring cloud官方文档中的话
Spring Cloud Config为分布式系统中的外部化配置提供服务器端和客户端支持。使用Config Server,您可以在所有环境中管理应用程序的外部属性。客户端和服务器上的概念映射与Spring Environment和PropertySource抽象,因此它们非常适合Spring应用程序,但可以与任何语言运行的任何应用程序一起使用。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配置,并确保应用程序具有迁移时需要运行的所有内容。服务器存储后端的默认实现使用git,因此它可以轻松支持配置环境的标签版本,以及可用于管理内容的各种工具。添加替代实现并使用Spring配置插入它们很容易。
简单来说Spring Cloud Config 是微服务中用来外部管理应用配置文件的一种解决方案.
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.0.BUILD-SNAPSHOTversion>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Finchley.BUILD-SNAPSHOTspring-cloud.version>
properties>
为了实现微服务应用配置的外部管理,我们首先肯定需要一个管理这些配置的一个微服务,而spring config server 就是这样的一个微服务
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
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: "*"
之后就是创建需要被管理配置文件的项目了,这里我创建的项目名字叫order项目spring版本与之前一样
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
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
文件
并且在其中添加内容
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)
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)
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)
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)
我们要用的就是这个/actuator/bus-refresh
它是一个post接口,那么我们利用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
明显我们需要指定一个让github发送请求的一个外网地址,但是我们在内网测试时明显没有外网地址,这里我们使用一个NATAPP小程序 https://natapp.cn/
4.启动客户端
首先找到自己客户端的authtoken
在客户端根目录下打开cmdnatapp -authtoken=9ab6b9040a624f40
等号后面的token值是你们自己的token值
之后我们就可以获得一个外网地址,这里我把这个外网地址映射到了我自己电脑上的8078端口上。也就是我config server的端口。
那么我们就可以把这个外网的地址交给github了(注意,这个是免费隧道,每次重启都会换外网地址,所以仅供测试使用)
我们让github发送一个json请求到我们服务器。
此时我们修改一下github上的配置文件,来做一下测试。
发现NATAPP控制台输出400
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官方文档
发现官方提示,如果要使用github gitlab之类的webhook会发送POST请求以及携带JSON body,我们需要添加spring-cloud-config-monitor
的依赖,然后让github访问/monitor
接口。
此时会发现config server的控制台中输出日志,成功从github中更新配置文件到本地。但是!此时我们会发现order项目中并没有与预期一样输出日志更新配置文件,访问之前测试的env/print端口发现还是旧的配置文件。(这里就是我踩坑的内容了)
因为之前的内容和教程上还是差不多一样,并没有出现太大问题,而我的版本也和教程上的一样,但是教程上显示order项目也同时更新了配置文件,但是我本地确实没有更新,此时我决定去查看一下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名字都是order:dev 或者order-dev,但是我们的应用名叫order,是不是这个原因,于是我手动去github上新建了一个名叫order.yml的配置文件,在点击保存时,发现order项目成功热更新配置文件。原来是应用名字的原因导致order服务没有被成功接收到消息。
解决方案:
这里的解决方案有2种,请自行选择。(我个人用的是第二种方法)