乐趣区

SpringCloud统一配置中心

本篇简介
通过上两篇的介绍我们已经掌握了 SpringCloud 中的注册中心组件 Eureka 以及服务间的调用方式 RestTemplate 和 Feign。那么通过这两篇的内容,我们基本可以满足一些简单项目的开发需求了。但同样上述的项目架构还是有一些问题的。例如:

不方便维护: 因为在公司开发项目时,是有多个团队多个成员同时开发的,这样就避免不了如果有人修改项目的相关配置,那么可能会对其它项目组产生影响,甚至可以会导致项目代码冲突。
信息不安全: 因为按照我们之前的项目架构,我们是需要将所有配置写到项目中的。例如数据库账号及密码等。但如果将这些敏感的数据信息放到项目中的话,难免会有一些安全性的问题。因为所有项目开发者都有这些敏感信息的所有权限。
开发效率低: 因为我们之前的文章中介绍过了,每当我们修改配置文件时,都是需要重新启动项目的。当我们平时开发时还好,但如果修改生产已经运行的项目配置的话,那么就会涉及到项目上线及其重启,这可能会导致一些线上问题的产生。

SpringCloud 为了解决上述的问题,于是提供了统一注册中心组件 Config。通过这个组件,不同的服务都可以从这个组件获取相关的配置信息。而这个组件也不存储其它服务的配置信息,而是通过远程仓库获取相关的配置信息。例如:Git、Svn 等。这样当我们修改不同服务的配置信息时,我们只要在远程仓库更改后,其它的服务也就可以通过配置中心获取到最新的配置信息了。并且这样还有一个好处,就是我们可以通过远程仓库来设置不同人员的权限,这样就解决了敏感信息的泄漏问题了。下面我们来详细介绍一下怎么搭建一个配置中心。

搭建配置中心
还是和之前项目一样创建一个 SpringBoot 的项目。

这一步骤也是和之前创建的项目一样,设置项目相关参数。

因为我们配置中心也是一个 Eureka 的客户端,所以我们在这一步还要选择 Eureka 的客户端的依赖。
除了要添加 Eureka 的客户端的依赖外,还要添加一个非常重要的依赖,也就是统一配置中心的组建 Config 依赖。
这一步骤没什么介绍的了,只要点击完成就可以了。


这样我们就完成了配置中心的创建了,但这还没完。我们还没有搭建完一个配置中心。因为按照之前我们搭建注册中心的方式,我们还是需要在启动类上添加配置中心的注解的,下面我们看一下启动类的源码:
package com.jilinwula.springcloudconfig;

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

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class SpringcloudConfigApplication {

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

}

因为配置中心是一个 Eureka 的客户端,所以我们需要在启动类上添加了 @EnableEurekaClient 注解。还有另一个注解就是配置中心的核心注解也就是 @EnableConfigServer。下面我们看一下该项目的配置信息:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: springcloud-config
server:
port: 8081
配置信息很简单和一个普通的 Eureka 客户端的配置一样,下面我们启动这个项目来看一下项目的运行结果。下面为项目启动日志:
Error starting ApplicationContext. To display the conditions report re-run your application with ‘debug’ enabled.
2019-03-17 18:41:54.442 ERROR 1522 — [main] o.s.b.d.LoggingFailureAnalysisReporter :

***************************
APPLICATION FAILED TO START
***************************

Description:

Invalid config server configuration.

Action:

If you are using the git profile, you need to set a Git URI in your configuration. If you are using a native profile and have spring.cloud.config.server.bootstrap=true, you need to use a composite configuration.

当我们满怀期待的觉的项目可以正常运行的时候,我们发现项目启动居然报错了。这是为什么呢?按照我们之前搭建注册中心时我们的理解。配置中心是不是也会提供一个什么管理界面之类的。但结果让我们很意外,程序居然抛出了异常。这到底是为什么呢?这是因为我们之前介绍过配置中心也不会保存其它服务的配置信息的,而是从远程仓库中获取配置信息,然后将配置信息暂存到本地,然后在对外提供服务。所以上述错误的根本原因是因为我们没有配置远程仓库的地址。如果我们观察仔细的话,我们可以通过上述的错误日志发现,日志已经提示我们没有配置远程仓库的地址了,也就是日志中的 Git URI。下面我们配置一下配置中心的远程仓库地址。在配置之前,我们首先要创建一个远程仓库。


创建配置中心仓库
创建远程仓库可以有很多种方式。比较常用的例如:GitHub、GitLab 等。为了演示方便我们就不在手动搭建 Git 服务器了,而是直接使用免费的 GitHub 了。下面我们看一下怎么在 GitHub 中创建远程仓库。我们首先访问 GitHub 的官网。下面为官方地址:
https://github.com
在使用 GitHub 创建仓库时需要先注册账号,因为我本人已经注册过了,所以我们就直接演示登陆了。也就上图中红色标识的地方。
当我们登陆成功后,就会显示自己的个人主页,也就是上图所示,我们直接点击上图中红色表示,来创建一个新的仓库。当我们输完仓库的名字及其仓库描述后,然后点下方绿色的按钮即可。当我们创建完仓库后,我们点击上图中红色的链接,在该仓库中创建一个新的文件,然后将服务的相关配置写到这个文件中。
我们直接点击下图中的绿色按钮即可。下图是保存完文件后返回的页面。我们看上图中浏览器地址栏显示的路径不是我们默认创建的仓库地址,而显示了分支的名字。我们在配置中心配置时是不能配置这个地址的。我们点击一下仓库地址。这样浏览器地址栏就会显示仓库正确的地址了。也就是下图中的地址。


指定配置中心仓库地址
既然配置中心的仓库我们已经创建完了,下面我们将该仓库的地址配置到配置中心的项目中,然后在启动一下项目看看项目还会不会抛出异常。下图为配置中心仓库的配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: springcloud-config
cloud:
config:
server:
git:
uri: https://github.com/jilinwula/springcloud-config
server:
port: 8081
如果我们此时启动项目并观察启动日志,我们会发现日志在启动时已经不会抛出异常了,这就说明我们的远程仓库配置成功了。下面我们看一下怎么访问配置中心返回的配置。


访问配置中心
按照我们之前的理解,我们直接访问该项目的端口,然后看看会返回什么样的信息。下面为访问路径:
GET http://127.0.0.1:8081
返回结果:
GET http://127.0.0.1:8081

HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Mar 2019 03:27:23 GMT

{
“timestamp”: “2019-03-18T03:27:23.930+0000”,
“status”: 404,
“error”: “Not Found”,
“message”: “No message available”,
“path”: “/”
}

Response code: 404; Time: 81ms; Content length: 121 bytes

我们看程序居然又抛出了异常,但这异常我们应该比较常见了,因为之前文章中都介绍过,这是因为我们没有配置 Controller 导致。实际在配置中心中我们是不需要写任何 Controller 的,甚至连一行代码都不需要写的。因为上面已经介绍过,配置中心只是将远程仓库的配置信息拉取到本地,然后在对外提供服务。这时可能有人会想,既然不需要写 Controller 那我们怎么访问配置中心返回的配置信息呢? 不用着急,既然配置中心不需要我们写任何代码而可以支持其它服务访问配置信息,那结果一定是 SpringCloud 默认为我们封装好了,我们只需要按照配置中心中默认的访问规则就可以返回配置信息的结果了。下面我们看一下配置中心的访问规则。


配置中心访问规则
还记着我们在远程仓库中新创建的 client.yml 文件吗?我们已经将相关的配置信息添加到了该文件中。配置中心实际上就是返回该文件的内容。也就是通过配置中心里的 Git URI 找到仓库中的配置文件。所以下面我们直接访问这个文件看看会返回什么结果。
GET http://127.0.0.1:8081/client.yml
返回结果:
GET http://127.0.0.1:8081/client.yml

HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Mar 2019 03:42:49 GMT

{
“timestamp”: “2019-03-18T03:42:49.473+0000”,
“status”: 404,
“error”: “Not Found”,
“message”: “No message available”,
“path”: “/client.yml”
}

Response code: 404; Time: 30ms; Content length: 131 bytes

我们看接口的返回还是出错。刚刚不是说 SpringCloud 自动为我们处理吗? 这是因为我们在通过配置中心访问远程仓库文件时有一个特殊的规则。就是: 仓库文件名 - 随机名. 后缀。只有这样配置中心才会正确的返回远程仓库中的配置信息。也就是下面的访问路径:

http://127.0.0.1:8081/client-test.yml
返回结果:
GET http://127.0.0.1:8081/client-test.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 156
Date: Mon, 18 Mar 2019 03:59:35 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
server:
port: 8082
spring:
application:
name: springcloud-client

Response code: 200; Time: 2306ms; Content length: 156 bytes

我们看这时接口就成功的返回远程仓库中的配置信息了。为了更直观的感受,我们修改一下远程仓库的配置,然后在请求一下上述的接口,看看还能不能返回仓库中最新的配置。下面为远程仓库中配置文件中的更改。原仓库配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: springcloud-client
server:
port: 8082
现仓库配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: springcloud-client
server:
port: 8082
userinfo:
username: jilinwula
password: 123456789
下面我们继续访问下面的接口,然后看一下配置中心返回的结果。
GET http://127.0.0.1:8081/client-test.yml
返回结果:
GET http://127.0.0.1:8081/client-test.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 210
Date: Mon, 18 Mar 2019 07:10:10 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
server:
port: 8082
spring:
application:
name: springcloud-client
userinfo:
password: 123456789
username: jilinwula

Response code: 200; Time: 4266ms; Content length: 210 bytes

我们看配置中心已经成功的返回了远程仓库中最新的配置信息了。(备注: 特别注意如果我们在远程仓库中的配置信息包含中文的话,配置中心在返回时可能会导致中文乱码,这种问题我们在之后的文章中在做介绍)。下面我们介绍了一个配置中心返回配置的规则。全部的路径为:
/{label}/{application}-{profile}.yml
下面我们详细介绍上面参数的含义:

label: 远程仓库的分支名字。如果我没有指定该参数默认按照 master 访问,但在访问时,如果分支为 master 可以省略。
application: 远程仓库文件的名字,也就是上图中的 client。
profile: 不同环境的名字。例如:dev(开发环境)、test(测试环境)、pro(生产环境) 等。

这时可能有人会产生疑问?上面的远程仓库中也没有什么 client-dev.yml 文件啊为什么我们访问这个路径还可以成功的返回配置信息呢?这里面还有一个默认的约定。那就是配置中心在从远程仓库获取配置信息时,除了要获取 client-dev.yml 文件外,还会获取 client.yml。然后将这两份文件的内容合并在一起给服务返回。因为我们没有 client-dev.yml 文件,所以我们在访问 client-dev.yml 时就会直接获取 client.yml 文件的内容。为了测试我们上面所说的,我们在远程仓库新创建一个 client-dev.yml 文件,然后我们故意将这两份文件的内容编写的不一致。然后看一下配置中返回的结果。client-dev.yml:
profile:
active: dev
访问以下接口地址:
GET http://127.0.0.1:8081/client-dev.yml
返回结果:
GET http://127.0.0.1:8081/client-dev.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 233
Date: Mon, 18 Mar 2019 08:05:39 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
profile:
active: dev
server:
port: 8082
spring:
application:
name: springcloud-client
userinfo:
password: 123456789
username: jilinwula

Response code: 200; Time: 2360ms; Content length: 233 bytes
我们看配置中心返回的结果已经包含远程仓库中 client-dev.yml 的内容了。那此时如果我们访问 client-test.yml 呢?会返回什么样的结果呢? 我们验证一下。
GET http://127.0.0.1:8081/client-test.yml 返回的结果:
GET http://127.0.0.1:8081/client-test.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 210
Date: Mon, 18 Mar 2019 08:08:19 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
server:
port: 8082
spring:
application:
name: springcloud-client
userinfo:
password: 123456789
username: jilinwula

Response code: 200; Time: 2291ms; Content length: 210 bytes
我们看配置中心返回的结果还是 client.yml 文件而没有返回 client-dev.yml 的内容。但为什么配置中心要这样处理呢?这样处理有什么好处呢?如果我们熟悉 SpringBoot 开发我们就会知道 application.yml 文件,该文件就是不同环境的共有文件。我们可以将不同环境配置的信息配置在这个文件中,这样就减少了不同环境中有大量相同配置的问题了。所以配置中心中的上述功能也是解决这样问题的。除了上述的功能外,配置中心还对远程仓库中返回的数据的格式做了优化。因为按照我们远程仓库中的名字是 yml 文件。但配置中心可以直接支持返回 json 格式的配置信息。也就是下面的请求路径:
GET http://127.0.0.1:8081/client-dev.json
返回结果:
GET http://127.0.0.1:8081/client-dev.json

HTTP/1.1 200
Content-Type: application/json
Content-Length: 246
Date: Mon, 18 Mar 2019 08:19:11 GMT

{
“eureka”: {
“client”: {
“service-url”: {
“defaultZone”: “http://127.0.0.1:8761/eureka”
}
}
},
“profile”: {
“active”: “dev”
},
“server”: {
“port”: 8082
},
“spring”: {
“application”: {
“name”: “springcloud-client”
}
},
“userinfo”: {
“password”: 123456789,
“username”: “jilinwula”
}
}

Response code: 200; Time: 3762ms; Content length: 246 byte
我们看这时配置中心返回的格式就是 json 类型了。上述内容就是配置中心的访问规则,当然我们还没有测试不同分支的情况,在这里就不依依演示了,和上述的内容基本一致。下面我们介绍一下怎么在客户端服务中使用配置中心返回配置。


配置中心客户端配置
为了方便测试,我们需新建一个 Eureka 客户端项目,然后在这个项目中新创建的项目中使用配置中心获取该项目的相关配置。因为我们之前已经介绍过了怎么创建一个 Eureka 客户端项目了。在这里我们就不演示怎么创建了。而是直接在该项目中添加配置中心的依赖。具体配置如下:pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
application.yml:
spring:
application:
name: client
cloud:
config:
discovery:
enabled: true
service-id: springcloud-config
profile: dev

下面我们详细介绍一下上述配置中参数的说明:

service-id: 配置中心中的项目名字
profile: 对应远程仓库中环境的名字
name: 远程仓库里配置名字的名字

正是上面这 3 个参数的配置,客户端就可以按照配置中心访问的规则获取远程仓库的信息。下面我们启动项目并访问一下注册中心看一下该服务是否注册成功。我们看注册中心已经成功的检测到了该服务。下面我们试一下能不能正确的通过配置中心获取远程仓库的配置。我们在项目中新创建一个用户类然后按照我们之前介绍过的知识,将配置文件中的信息读取到该类属性中,然后通过 Controller 返回该类的信息。下面为具体代码:UserInfo:
package com.jilinwula.springcloudclient;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(“userinfo”)
public class UserInfo {
private String username;
private String password;
}

UserInfoController:
package com.jilinwula.springcloudclient;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(“/userinfo”)
public class UserInfoController {

@Autowired
private UserInfo userInfo;

@GetMapping(“/get”)
public Object get() {
return userInfo;
}
}

下面我们访问一下 UserInfoController 看一下能否返回远程仓库的配置。
GET http://127.0.0.1:8082/userinfo/get
返回结果:
org.apache.http.conn.HttpHostConnectException: Connect to 127.0.0.1:8082 [/127.0.0.1] failed: Connection refused (Connection refused)
我们接口返回的结果居然抛出了异常。这是为什么呢? 这是因为程序在启动时通过 @ConfigurationProperties 注解直接获取项目中配置文件中的配置。但又由于我们的配置是从配置中心中远程仓库获取的,所以该注解就获取不到配置中心中的配置了。那怎么解决呢? 其实解决这样的问题也很简单,我们只要保证在注解获取配置时,项目已经从配置中心获取到了配置就可以了。那怎么保证呢?SpringBoot 为了解决配置加载的顺序的问题,于是提供了除 application.yml 文件外,还提供了 bootstrap.yml 文件,并且默认优先加载 bootstrap.yml 文件。也就是在程序启动时就已经加载完了。这样就解决了上述的错误了。下面我们将 application.yml 文件名字修改为 bootstrap.yml 文件在访问一下上述接口。
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Mar 2019 15:36:14 GMT

{
“username”: “jilinwula”,
“password”: “123456789”
}

Response code: 200; Time: 147ms; Content length: 47 bytes

我们看接口这次返回远程仓库中的配置了。为了更直观的感受,我们在修改一下远程仓库中的配置信息,然后在访问一下该接口。下面为远程仓库配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
server:
port: 8082
userinfo:
username: admin
password: 123456789

我们将 username 参数的值由 jilinwula 修改成了 admin。下面我们在访问一下接口看一下返回结果。
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Mar 2019 15:42:33 GMT

{
“username”: “jilinwula”,
“password”: “123456789”
}

Response code: 200; Time: 14ms; Content length: 47 bytes

我们发现接口返回的居然还是之前的信息。这是为什么呢?这个原因是项目只有在启动时才通过配置中心获取远程仓库中的信息。我们虽然改了远程仓库中的信息,但该项目获取的还是上一次启动时的远程仓库的信息,所以该接口返回的还是之前的信息。所以解决这样的问题很简单,我们只要重启一下项目就可以了。下面我们启动项目后在看一下该接口的返回结果。
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 18 Mar 2019 15:44:27 GMT

{
“username”: “admin”,
“password”: “123456789”
}

Response code: 200; Time: 131ms; Content length: 43 bytes

我们看这时该接口就正确的返回了远程仓库中最新的配置信息了。下面我们介绍一下多配置中心的配置。

多配置中心
为了解决让程序高可用,通常在项目开发时,如果只有一个配置中心是不稳定的。如果该配置中心出现了问题就会导致整个项目运行失败。所以我们通常会配置多个配置中心。下面我们创建第二个配置中心。我们同样不演示配置中心具体的创建了,而是直接看该配置中心的配置。
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: springcloud-config
cloud:
config:
server:
git:
uri: https://github.com/jilinwula/springcloud-config
server:
port: 8083
下面我们启动该项目并看一下注册中心看看是否注册成功。下面我们启动客户端服务看一下客户端服务会调用哪个配置中心获取配置信息。下面启动日志。
2019-03-19 10:01:07.493 INFO 13533 — [main] c.c.c.ConfigServicePropertySourceLocator : Multiple Config Server Urls found listed.
2019-03-19 10:01:07.494 INFO 13533 — [main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://bogon:8081/
我们看客户端的日志输出了 8081 端口,这也就是说当前项目调用的是 8081 端口的配置中心服务。如果我们多次重启项目,我们就会发现客户端服务获取配置中心的服务端口是随机变化的。这也就是说明配置中心服务的负载策略是随机,至于怎么修改,在这里我们就不详细介绍了,如有不了解的,可以看一下注册中那篇文章中的负载策略。


修改注册中心默认端口
我们知道注册中心的默认端口为 8761。那如果我们修改了注册中心中的默认端口然后在启动客户端的项目会怎么样呢? 下面我们测试一下。下面我们将配置中心的端口修改为 8084。然后在启动一下客户端的服务看一下启动日志。(注意别忘记将配置中心里的注册中心的地址修改为 8084)。下面我客户端项目的启动日志。
2019-03-19 10:22:57.833 ERROR 13635 — [main] c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{serviceUrl=’http://localhost:8761/eureka/}

com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused (Connection refused)
我们发现,我们只是修改了注册中心的默认端口,客户端在启动的时候,居然抛出了异常了。并且通过启动日志发现,客户端里居然还是调用了 8761 端口。这是为什么呢?如果我们仔细想想,好像有一个注册中心的配置的地方没有改。也就是远程仓库中的地址。下面我们访问下面的地址看一下远程仓库中的配置。
GET http://127.0.0.1:8081/client-dev.yml
返回结果:
GET http://127.0.0.1:8081/client-dev.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 154
Date: Tue, 19 Mar 2019 02:30:37 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
server:
port: 8082
userinfo:
password: 123456789
username: admin

Response code: 200; Time: 2638ms; Content length: 154 bytes

我们发现远程仓库中的注册中心地址还是 8761 端口。下面我们将远程仓库中的注册中心端口也修改为 8084。然后我们在访问上述端口看一下配置中心返回的配置是不是最新的。
GET http://127.0.0.1:8081/client-dev.yml
返回结果:
GET http://127.0.0.1:8081/client-dev.yml

HTTP/1.1 200
Content-Type: text/plain
Content-Length: 154
Date: Tue, 19 Mar 2019 02:36:32 GMT

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8084/eureka
server:
port: 8082
userinfo:
password: 123456789
username: admin

Response code: 200; Time: 3622ms; Content length: 154 bytes

现在我们在启动一下客户端项目看一下启动日志。
2019-03-19 10:43:05.672 ERROR 13686 — [nfoReplicator-0] c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{serviceUrl=’http://localhost:8761/eureka/}

com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused (Connection refused)
我们发现项目启动时还是会抛出异常,并且日志显示还是获取的是 8761 端口。这时有人就会感觉到奇怪了,为什么还会抛出异常呢? 我们已经将项目中所有注册中心配置的地方都修改为了 8084 了。为什么项目启动时还找 8761 端口呢? 我们再看一下客户端中的配置信息,然后在分析一下。
spring:
application:
name: client
cloud:
config:
discovery:
enabled: true
service-id: springcloud-config
profile: dev

上面的配置我们之前介绍过,主要是通过 name+profile 参数来通过配置中心返回远程仓库的信息。但这里我们忽略了一个很重要的问题。就是客户端本身首先要通过注册中心获取到配置中心的地址,然后在通过配置中心在获取远程仓库的配置。上面我们的配置中没有指定注册中心的地址,所以程序在启动时就会抛出异常。因为它根本就获取不到配置中心。这时可能有人会问,那为什么我们没有改注册中心默认端口时,项目启动不会抛出异常呢?这是因为 SpringCloud 默认会找项目中配置文件里的注册中心地址,如果没有找到则调用默认的注册中心地址。如果找到了就按照配置文件中注册中心地址进行服务调用。所以我们之前的项目在启动时是不会抛出异常的。既然我们知道了问题所在,那解决就比较容易了,我们只要将远程仓库中注册中心的配置放到项目里就可以了。下面为客户端项目配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8084/eureka
spring:
application:
name: client
cloud:
config:
discovery:
enabled: true
service-id: springcloud-config
profile: dev

这时我们在启动项目时就会发现项目可以正常启动了,如些此时查看注册中心发现客户端服务已经在注册中心注册成功了。


自动更新配置
我们之前介绍过如果我们直接更改远程仓库的配置时,客户端是获取不到最新的配置的。只有当我们重启自动客户端服务才可以。但这明显不太方便。下面我们分析一下为什么当远程仓库更新配置时,客户端获取不到最新的配置。答案很简单,因为服务只有在启动的时候才通过配置中心获取远程仓库的配置,当服务启动后,如果远程仓库中的配置进行了修改,客户端服务因为在启动时已经获取了配置信息了,现在就不会在获取了。所以也就不会获取到最新的配置了。那怎么解决了呢?解决的方式很简单,就是每当远程仓库配置做修改后,都通知客户端服务,这样客户端服务就可以获取到最新的配置信息了。那怎么通知呢? 那就是使用消息队列。每当远程仓库更新时,都向消息队列中发起一条消息,然后客户端服务发现消息队列里的消息后,然后在重新获取最新的配置。那我们使用哪个消息队列呢?SpringCloud 默认为我们支持了 RabbitMQ。当然我们也可以使用其它的消息队列,只不过如果使用 RabbitMQ 的话,SpringCloud 会为我们提供自动化默认配置。也就是我们什么都不需要配置就可以直接使用该队列服务。下面我们看一下具体怎么配置。如果要使用队列服务时,那就需要在使用的项目中添加相应的依赖,下面我们看一下配置中心里依赖的修改。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
我们只要在配置中心的项目里添加上面的依赖就可以。然后我们同样在客户端的服务中添加上面的依赖。然后我们启动项目后,在看一下消息队列里的消息是否有变化。怎么安装 RabbitMQ 的内容我们就不在这里做详细介绍了,我们直接访问消息队列的管理界面即可。下面地址为 RabbitMQ 默认的管理界面地址:

http://127.0.0.1:15672
我们看上图中的消息队列中默认多了两个队列并对应着配置中心服务和客户端服务。如果我们这时修改远程仓库中的配置,然后在请求客户端接口时,那么客户端获取还是和之前一样是旧的配置信息。虽然我们使用了队列但是当配置中心修改时,消息队列也不知道配置进行了修改,所以我们需要一个触发点让消息队列知道配置信息进行了更改。在 SpringCloud 中于是提供了 bus-refresh 接口,该接口的目的就是告诉配置中心,远程仓库中的配置进行了修改。下面我们访问一下该接口。
POST http://127.0.0.1:8081/actuator/bus-refresh
返回结果:
POST http://127.0.0.1:8081/actuator/bus-refresh

HTTP/1.1 405
Allow: GET
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 19 Mar 2019 13:45:28 GMT

{
“timestamp”: “2019-03-19T13:45:28.346+0000”,
“status”: 405,
“error”: “Method Not Allowed”,
“message”: “Request method ‘POST’ not supported”,
“path”: “/actuator/bus-refresh”
}

Response code: 405; Time: 142ms; Content length: 165 bytes

当我们访问 SpringCloud 为我们提供的 bus-refresh 接口时,居然抛出了异常。这是为什么呢?这是因为 SpringCloud 默认配置了访问权限,并且该权限默认是禁止访问的。所以我们在使用 bus-refresh 接口时,必须先将该接口的访问权限设置为允许,这样就可以了访问该接口了。下面为配置中心里配置的修改:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8084/eureka
spring:
application:
name: springcloud-config
cloud:
config:
server:
git:
uri: https://github.com/jilinwula/springcloud-config
server:
port: 8081
management:
endpoints:
web:
exposure:
include: “bus-refresh”
下面我们重新启动配置中心后,在访问一下下面的接口。
POST http://127.0.0.1:8081/actuator/bus-refresh
返回结果:
POST http://127.0.0.1:8081/actuator/bus-refresh

HTTP/1.1 204
Date: Tue, 19 Mar 2019 13:56:43 GMT

<Response body is empty>

Response code: 204; Time: 3487ms; Content length: 0 bytes

我们看这时接口就访问正常了。下面我们测试一下,看看这样的方式,会不会自动更新配置。我们首先访问下面客户端接口看一下该接口返回的信息是什么?
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 19 Mar 2019 14:23:34 GMT

{
“username”: “admin”,
“password”: “12345”
}

Response code: 200; Time: 134ms; Content length: 39 bytes

下面我们修改一下远程仓库的配置信息。
server:
port: 8082
userinfo:
username: admin
password: 54321
如果我们此时在访问上述的接口的话,那该接口返回的结果一定还是早的信息。这是咱们之前测试过的。下面我们先调用一下 bus-refresh 接口。
POST http://127.0.0.1:8081/actuator/bus-refresh
返回结果:
POST http://127.0.0.1:8081/actuator/bus-refresh

HTTP/1.1 204
Date: Tue, 19 Mar 2019 14:28:52 GMT

<Response body is empty>

Response code: 204; Time: 3344ms; Content length: 0 bytes

我们这时在访问客户端接口看一下客户端能不能获取到最新的配置信息。
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 19 Mar 2019 14:30:20 GMT

{
“username”: “admin”,
“password”: “12345”
}

Response code: 200; Time: 13ms; Content length: 39 bytes

我们发现客户端服务还是没有获取到最新远程仓库中的配置,这又是怎么回事呢?这是因为我们还差最后一个环节,也就是需要在我们获取动态参数的地方添加 @RefreshScope 注解。只有添加了该注解,才能达到动态刷新的功能。下面我们在客户端的 Controller 中添加 @RefreshScope 注解。然后在重新启动一下客户端的服务。(备注: 特别注意当我们客户端服务重新启动时,就会重新通过配置中心获取远程仓库最新的配置了。所以我们为了测试动态刷新的功能,应该在该服务启动后,然后在重新修改一下远程仓库的配置,然后在调用 bus-refresh 接口,看一下客户端服务是否能获取到最新的配置)。下面为访问的客户端接口:
GET http://127.0.0.1:8082/userinfo/get
返回结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 19 Mar 2019 14:30:20 GMT

{
“username”: “admin”,
“password”: “54321”
}

Response code: 200; Time: 13ms; Content length: 39 bytes

下面我们将远程仓库中的参数修改一下然后在测试一下。
server:
port: 8082
userinfo:
username: admin
password: 12345
我们在访问一下客户端的接口。(备注: 不要忘记了我们应该先访问一下 bus-refresh 接口)。下面继续请求客户端接口:
GET http://127.0.0.1:8082/userinfo/get
返回的结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 19 Mar 2019 14:56:26 GMT

{
“username”: “admin”,
“password”: “12345”
}

Response code: 200; Time: 13ms; Content length: 39 bytes

我们看这回我们就达到了动态刷新的功能了。但我们发现还有一点不太方便就是每当我们修改远程仓库配置时,都要手动调用了 bus-refresh 接口。那有没有什么办法呢?答案一定还是有的。但这不是 SpringCloud 的功能,而是 GitHub 的功能。下面我们详细介绍一下。


Webhooks 配置
GitHub 中有一个功能就是当文件变更、提交、评论时,可以自动触发一个请求。我们正好可以利用这个功能来满足我们自动刷新的功能。下面我们看一下怎么配置 Webhooks。下面我们打开远程仓库中的项目。然后点击红色链接。我们看上面最后一张图中需要我们指定的 Payload URL 参数,也就是当远程仓库中有变化时,GitHub 就会调用我们指定的这个 URL。因为 Payload URL 参数是需要外网调用的,所以为了测试方便,我们直接使用了 NATAPP 工具,通过该工具可以做内网穿透,也就是通过该工具为我们分配的随机的域名可以映射到我们本地的接口。我们只要将该工具外网域名地址映射到我们配置中心的端口即可。下面我们在更新一下远程仓库中的配置,然后直接访问客户端的接口,看看能不能直接获取到最新的远程仓库中的配置。
server:
port: 8082
userinfo:
username: jilinwula
password: jilinwula

下面我们直接访问客户端的服务的接口看一下返回的结果。
GET http://127.0.0.1:8082/userinfo/get
返回的结果:
GET http://127.0.0.1:8082/userinfo/get

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 20 Mar 2019 05:46:34 GMT

{
“username”: “admin”,
“password”: “12345”
}

Response code: 200; Time: 204ms; Content length: 49 bytes

我们发现客户端服务获取的配置信息还是旧的配置,还是没有获取到最新的配置,这是为什么呢?我们查看一下 GitHub 上的 WebHook,在网页下面居然有警告信息。我们看一下这警告信息是什么错误。
{“timestamp”:”2019-03-19T16:02:46.565+0000″,”status”:400,”error”:”Bad Request”,”message”:”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\n at [Source: (PushbackInputStream); line: 1, column: 300] (through reference chain: java.util.LinkedHashMap[\”commits\”])”,”path”:”/actuator/bus-refresh”}
我们看上述报的错误是格式化的错误。这是为什么呢?我们在本地调用一下这个接口看一下返回的结果。
POST http://mzqha4.natappfree.cc/a…

返回的结果:
POST http://mzqha4.natappfree.cc/actuator/bus-refresh

HTTP/1.1 204
Date: Wed, 20 Mar 2019 06:15:14 GMT

<Response body is empty>

Response code: 204; Time: 4074ms; Content length: 0 bytes

我们看这是该接口返回的结果。但我们发现上述接口中 Response 返回的 Code 码是 204,意味着返回的结果是空的。因为该接口压根就不需要有返回值。如果我们本地调用该接口的话,没有任何问题,但是 WebHook 中对返回的 Code 码有要求,必须返回 200 才会认为该请求发送成功。那怎么解决呢? 很简单,我们自己在配置中心新创建一个 Controller,让 WebHook 直接调用这个 Controller 中的接口,然后我们在这个 Controller 中自己在调用一下 actuator/bus-refresh 接口。因为是我们自己写的 Controller 了,所以我们可以很方便的控制该接口的返回值,这样也就能保证该接口返回的 Code 码为 200 了。(备注: 只要我们正常返回数据,Code 码默认就会是 200)。下面我们看一下这个 Contrller 中的源码:
package com.jilinwula.springcloudconfig;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(“/config”)
public class ConfigController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@Autowired
private RestTemplate restTemplate;

@PostMapping(“/sync”)
public Object sync() {
Map<String, Integer> map = new HashMap<String, Integer>();
ServiceInstance serviceInstance = loadBalancerClient.choose(“springcloud-config”);
String url = String.format(“http://%s:%s/actuator/bus-refresh”, serviceInstance.getHost(), serviceInstance.getPort());
restTemplate.postForObject(url, map, Object.class);
map.put(“code”, 0);
return map;
}
}

上述的代码比较简单我们就不详细介绍了。下面我们先在本地调用一下,看一下该接口是否能够达到自动刷新配置。
POST http://127.0.0.1:8081/config/sync
返回结果:
POST http://127.0.0.1:8081/config/sync

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 20 Mar 2019 06:57:58 GMT

{
“code”: 0
}

Response code: 200; Time: 54531ms; Content length: 10 bytes

我们看这回接口返回的 Code 码为 200 了。下面我们将上述接口配置到 WebHook 中。然后我们在修改一下配置,看看 WebHook 中是否还有警告信息。我们看这回 WebHook 不显示警告信息了。如果我们这时在访问一下客户端的服务,我们就会发现,客户端接口可以返回最新的配置信息了。

上述内容就是本篇的全部内容,如有不正确的地方欢迎留言,谢谢。


源码地址
https://github.com/jilinwula/jilinwula-springcloud-config









退出移动版