1 Preface

Configurability is a feature that a mature software system should provide, and configuration management is important for large systems, especially for microservices systems with multiple applications. Happily, Spring provides us with good configuration management, such as the powerful configuration of Springboot. For Spring Cloud, there is the powerful Spring Cloud Config which is very useful for distributed system configuration management by providing a configuration management outside the application, such as a file or Git repository.

2 Quick Experience

The Spring Cloud Config server is a Springboot application that is very easy to start and deploy.

The overall architecture is shown in the following figure.

Spring Cloud Config

2.1 The server side is a Springboot

Add a dependency to Springboot as follows.

1
2
3
4
5
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-config-server</artifactId>
  <version>2.2.0.RELEASE</version>
</dependency>

That’s all, it already contains web and actuator.

Add the Java main class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.pkslow.config;

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

@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServer.class,args);
    }
}

Compared to a normal Springboot application it just has one more annotation @EnableConfigServer.

2.2 Configuration repositories

We will manage the configuration through version control, generally using the Git repository, but for simplicity we will use a local repository as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 创建目录
mkdir git-repo

# 初始化一个git目录
git init

# 新建文件
touch application.properties

# 添加变更
git add .

# 提交变更
git commit -m "init"

Configure the project’s application.properties, note that it’s for the Config Server project and not in the git-repo directory.

1
2
3
server.port=8888
spring.application.name=config-server
spring.cloud.config.server.git.uri=/Users/pkslow/IdeaProjects/pkslow-modules/config-server/git-repo

Then you can start Config Server.

But there is nothing in the git repository for the config file, so we add the following and commit it (you have to commit it or it won’t be available).

1
2
3
pkslow.webSite=www.pkslow.com
pkslow.age=18
pkslow.email=admin@pkslow.com

2.3 Configuration path matching

So how do we get these configurations? It can be read from the following URLs.

1
2
3
4
5
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
  • label, which refers to a branch of code, such as master, feature-1, etc.
  • application, the name of the application, which will be used later when the client reads it.
  • profile, generally used to specify the environment, such as prod, dev, uat, etc.

So, we can use the following URL to get the configuration information we just added.

1
2
3
4
http://localhost:8888/application/default
http://localhost:8888/application/default/master
http://localhost:8888/master/application.properties
http://localhost:8888/application-default.properties

Use curl to access configuration information.

1
2
$ curl http://localhost:8888/application/default/master
{"name":"application","profiles":["default"],"label":"master","version":"8796f39b35095f6e9b7176457eb03dd6d62b1783","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/git-repo/application.properties","source":{"pkslow.webSite":"www.pkslow.com","pkslow.age":"18","pkslow.email":"admin@pkslow.com"}}]}

The last address /{label}/{application}-{profile}.properties returns the result in a different format, returning the profile contents directly.

1
2
3
4
$ curl http://localhost:8888/application-default.properties
pkslow.age: 18
pkslow.email: admin@pkslow.com
pkslow.webSite: www.pkslow.com

If we first create a branch release-20200809 and change age to 9, it will look like this.

1
2
$ curl http://localhost:8888/application/default/release-20200809
{"name":"application","profiles":["default"],"label":"release-20200809","version":"7e27e6972ed31ee1a51e9277a2f5c0a628cec67a","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/git-repo/application.properties","source":{"pkslow.webSite":"www.pkslow.com","pkslow.age":"9","pkslow.email":"admin@pkslow.com"}}]}

You can see that the corresponding pkslow.age has changed to 9, but access to /application/default/master is still 18, and the branches do not affect each other.

2.4 Remote repositories

The local repository is for simple demonstration purposes only. In real projects, you can create a new repository in GitHub as follows.

Github

A private repository was created to check if the authentication behind it is correct.

The address of the reconfigured repository is as follows.

1
2
3
4
5
spring.cloud.config.server.git.uri=https://github.com/pkslow/pkslow-config
spring.cloud.config.server.git.username=admin@pkslow.com
spring.cloud.config.server.git.password=***
spring.cloud.config.server.git.default-label=master
spring.cloud.config.server.git.search-paths=demo

Create a demo directory to hold the configuration, so search-paths is configured as demo. Finish the configuration and restart the server, and you can read the configuration of the remote repository normally.

2.5 Multiple code configuration repositories

There are times when our configuration may not be in just one repository, but in the code repository of the respective client, for example, we have the following three services.

  • (1) Service discovery: discovery, code repository pkslow-discovery-service
  • (2) API gateway: gateway, code repository pkslow-gateway-service
  • (3) Order service: order, code repository pkslow-order-service

Their respective configuration files are placed in their own code base, that requires the configuration of multiple code bases. We also configure one more default configuration library pkslow-default, if it does not match, the configuration of the default code base will be selected. The specific configuration is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server:
  port: 8888
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: /Users/pkslow/multiple-repos/pkslow-default
          repos:
            pkslow-discovery-service:
              pattern: pkslow-discovery-*
              cloneOnStart: true
              uri: /Users/pkslow/multiple-repos/pkslow-discovery-service
              search-paths: config
            pkslow-gateway-service:
              pattern: pkslow-gateway-*/dev
              cloneOnStart: true
              uri: /Users/pkslow/multiple-repos/pkslow-gateway-service
              search-paths: config
            pkslow-order-service:
              pattern: pkslow-order-*
              cloneOnStart: true
              uri: /Users/pkslow/IdeaProjects/pkslow-order-service
              search-paths: config

You can define the directory search-paths where the profiles are placed, and the default is the root directory if not configured.

The configuration rule for pattern here is {application}/{profile}, which supports the regular symbol *. Note that only one result is matched, if both are satisfied, only the first matching repository is taken.

After starting, let’s see the configuration result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 默认profile和label,正确匹配
$ curl http://localhost:8888/pkslow-order-service/default/master
{"name":"pkslow-order-service","profiles":["default"],"label":"master","version":"9d86e5d11974f0a0e7c20cd53d8f062755193e70","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-order-service/config/application.properties","source":{"pkslow.webSite":"www.pkslow.com","pkslow.app.name":"order-service"}}]}

# 正确匹配,但不存在的Label,配置库没有对应代码分支,404
$ curl http://localhost:8888/pkslow-order-service/default/release
{"timestamp":"2020-08-13T06:58:38.722+0000","status":404,"error":"Not Found","message":"No such label: release","path":"/pkslow-order-service/default/release"}

# profile为dev,正确匹配
$ curl http://localhost:8888/pkslow-order-service/dev/master
{"name":"pkslow-order-service","profiles":["dev"],"label":"master","version":"9d86e5d11974f0a0e7c20cd53d8f062755193e70","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-order-service/config/application.properties","source":{"pkslow.webSite":"www.pkslow.com","pkslow.app.name":"order-service"}}]}

# 对于gateway只能匹配profile=dev,无法匹配,读取默认配置
$ curl http://localhost:8888/pkslow-gateway-service/default/master
{"name":"pkslow-gateway-service","profiles":["default"],"label":"master","version":"8358f2b4701fac21a0c7776bc46cec6d9442c549","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-base/application.properties","source":{"pkslow.birthDate":"2020-08-10"}}]}

# 对于gateway只能匹配profile=dev,正确匹配
$ curl http://localhost:8888/pkslow-gateway-service/dev/master
{"name":"pkslow-gateway-service","profiles":["dev"],"label":"master","version":"1a4e26849b237dc2592ca0d391daaa1a879747d2","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-gateway-service/config/application.properties","source":{"pkslow.webSite":"www.pkslow.com","pkslow.app.name":"gateway-service"}}]}

# 不存在的服务名,无法匹配,读取默认配置
$ curl http://localhost:8888/unknown-service/dev/master
{"name":"unknown-service","profiles":["dev"],"label":"master","version":"8358f2b4701fac21a0c7776bc46cec6d9442c549","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-base/application.properties","source":{"pkslow.birthDate":"2020-08-10"}}]}

3 Client-side use of configuration

After the previous examples we have learned how the server side can get the configuration from the code base, but it is always important to enable the client side to get the corresponding configuration and produce the effect. Let’s demonstrate this.

3.1 Project preparation

Build the simplest Springboot Web project with Spring Cloud Config support and add the following dependencies.

1
2
3
4
5
6
7
8
<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>

Add the configuration file: bootstrap.properties (although we want to read the configuration from the server, some configurations must still be added at the client, such as the server address), with the following content.

1
2
3
server.port=8080
spring.application.name=pkslow-gateway-service
spring.cloud.config.uri=http://localhost:8888

Here we configure the port of the client, the address of the server and the name of the client application, the name is very critical and will be explained later.

Add Controller to expose the API to display the read configuration content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class PkslowController {
    @Value("${pkslow.age}")
    private Integer age;

    @Value("${pkslow.email}")
    private String email;

    @Value("${pkslow.webSite}")
    private String webSite;

    @GetMapping("/pkslow")
    public Map<String, String> getConfig() {
        Map<String, String> map = new HashMap<>();
        map.put("age", age.toString());
        map.put("email", email);
        map.put("webSite", webSite);
        return map;
    }
}

Then start the client and access the Controller. The result is as follows.

1
2
$ curl http://localhost:8080/pkslow
{"webSite":"default.pkslow.com","age":"9","email":"admin@pkslow.com"}

These configuration contents are not on the client side, indicating that the configuration information can be obtained from the server side.

3.2 How the client matches

The client has gotten the configuration information, is it correct? And how does the client match it? Actually, as mentioned in the previous section, there are three main pieces of information to match.

  • label refers to the code branch.
  • application is the name of the application.
  • profile is generally used to specify the environment.

The client name in the previous example is pkslow-gateway-service; label is not specified and defaults to master; profile is not specified and defaults to default.

Our server-side matching rule is pattern: pkslow-gateway-*/dev, so we can match the name, but not the profile, so the default repository configuration is read.

1
2
3
4
$ cat pkslow-base/application.properties 
pkslow.webSite=default.pkslow.com
pkslow.age=9
pkslow.email=admin@pkslow.com

Modify the client configuration by configuring profile to dev as follows.

1
2
3
4
server.port=8080
spring.application.name=pkslow-gateway-service
spring.profiles.active=dev
spring.cloud.config.uri=http://localhost:8888

Get the client configuration again as follows.

1
2
$ curl http://localhost:8080/pkslow
{"webSite":"gateway-master.pkslow.com","age":"9","email":"admin@pkslow.com"}

You can see that the configuration of the pkslow-gateway-service repository has been read.

1
2
3
4
$ cat pkslow-gateway-service/config/application.properties 
pkslow.webSite=gateway-master.pkslow.com
pkslow.age=9
pkslow.email=admin@pkslow.com

Create a new code branch release in the pkslow-gateway-service repository and add the configuration, then modify the client configuration as follows.

1
2
3
4
5
server.port=8080
spring.application.name=pkslow-gateway-service
spring.profiles.active=dev
spring.cloud.config.label=release
spring.cloud.config.uri=http://localhost:8888

After rebooting and accessing again, the configuration of the new branch was read correctly.

1
2
$ curl http://localhost:8080/pkslow
{"webSite":"gateway-release.pkslow.com","age":"9","email":"admin@pkslow.com"}

3.3 Client configuration takes effect instantly

When we modify the configuration, the result of accessing again is as follows.

1
2
3
4
5
6
7
8
9
$ git commit -a -m "update"
[release 0e489fe] update
 1 file changed, 2 insertions(+), 2 deletions(-)

$ curl http://localhost:8888/pkslow-gateway-service/dev/release
{"name":"pkslow-gateway-service","profiles":["dev"],"label":"release","version":"0e489fec5de73b1a6d11befa3f65e44836979e23","state":null,"propertySources":[{"name":"/Users/pkslow/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-gateway-service/config/application.properties","source":{"pkslow.webSite":"gateway-release.pkslow.com","pkslow.age":"10","pkslow.email":"admin@pkslow.com"}}]}

$ curl http://localhost:8080/pkslow
{"webSite":"gateway-release.pkslow.com","age":"9","email":"admin@pkslow.com"}

It turns out that the server side has taken effect, but the client side has not. This is because in this mode the client will only read the configuration at startup to make it effective. If we want the client to take effect as well, we need to use the /refresh endpoint provided by the Springboot actuator to do so.

Start by adding the actuator dependency.

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Activate and open the /refresh endpoint. Add the following configuration.

1
management.endpoints.web.exposure.include=refresh

Specify the scope of the refresh by adding the annotation @RefreshScope to the Controller.

1
2
3
4
5
@RefreshScope
@RestController
public class PkslowController {
//xxx
}

Restart the application. The operation and results are as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 修改配置并提交
$ git commit -a -m "update age to 18"
[release fc863bd] update age to 18
 1 file changed, 1 insertion(+), 1 deletion(-)

# 服务端配置生效
$ curl http://localhost:8888/pkslow-gateway-service/dev/release
{"name":"pkslow-gateway-service","profiles":["dev"],"label":"release","version":"fc863bd8849fa1dc5eaf2ce0a97afb485f81c2f0","state":null,"propertySources":[{"name":"/Users/larry/IdeaProjects/pkslow-modules/config-server/multiple-repos/pkslow-gateway-service/config/application.properties","source":{"pkslow.webSite":"gateway-release.pkslow.com","pkslow.age":"18","pkslow.email":"admin@pkslow.com"}}]}

# 客户端没有生效
$ curl http://localhost:8080/pkslow
{"webSite":"gateway-release.pkslow.com","age":"10","email":"admin@pkslow.com"}

# 发送POST请求到客户端/refresh
$ curl -X POST http://localhost:8080/actuator/refresh
["config.client.version","pkslow.age"]

# 客户端已经生效
$ curl http://localhost:8080/pkslow
{"webSite":"gateway-release.pkslow.com","age":"18","email":"admin@pkslow.com"}

3.4 Automatic configuration update

Every time you commit a configuration, you need to manually send a POST request to the client to update the configuration, which is obviously not friendly enough. GitHub provides a webhook function that sends a request to a specified URL after an event is triggered, so that automatic updates can be made.

Automatic updates via the webhook function are one-to-one, and cannot be implemented directly this way if there are many clients (which is usually the case). There are two options.

  1. Implement a port to accept requests from Git and distribute them to the servers. This is not recommended because it’s a bit tricky.
  2. Introduce Spring Cloud Bus to refresh multiple clients. This requires the introduction of MQ, such as kafka or RabbitMQ.

3.5 Services Discovery

In a microservices architecture, if both the server and the client are configured to register with service discovery (e.g. eureka), the client does not need to configure the address of the server anymore and will get the identification from the service discovery center. This is similar to Springboot Admin in the case of eureka.

There is nothing special about the code, it just registers both the server and the client to eureka.

4 Summary

This article shows how to use Spring Cloud Config step by step with examples, mainly to understand the interaction process and matching rules, everything else is code details, just refer to the official documentation.

Reference https://cloud.tencent.com/developer/news/680840