Skip to content
On this page

远程配置中心

官方文档:文档地址

Spring Cloud Config 为分布式系统中的外部配置提供服务器端和客户端支持。使用 Config Server,您可以集中管理所有环境中应用程序的外部配置。

实际上Spring Cloud Config就是一个配置中心,所有的服务都可以从配置中心取出配置,而配置中心又可以从GitHub远程仓库中获取云端的配置文件,这样我们只需要修改GitHub中的配置即可对所有的服务进行配置管理了。

服务端

这里我们接着创建一个新的项目,并导入依赖:

xml

<dependencies>
    <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>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11

启动类

java

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

配置文件

yaml
server:
  port: 8700
spring:
  application:
    name: config-server

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka

1
2
3
4
5
6
7
8
9
10
11

这里我们以本地仓库为例

shell
mkdir config.repo
git init
1
2

然后我们在文件夹中随便创建一些配置文件 common-datasource-dev.yml,并且要提交到刚才新建的仓库上。

yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://172.31.2.27:3306/cloud
    username: root
    password: root
1
2
3
4
5
6

然后我们在配置文件中,添加本地仓库的一些信息

yaml
spring:
  cloud:
    config:
      server:
        git:
          # 这里填写的是本地仓库地址,远程仓库直接填写远程仓库地址 http://git...
          uri: file://${user.home}/Desktop/config-repo
          # 默认分支设定为你自己本地或是远程分支的名称
          default-label: master
1
2
3
4
5
6
7
8
9

然后启动我们的配置服务器,通过以下格式进行访问:

shell
http://localhost:8700/{服务名称}/{环境}/{Git分支}
http://localhost:8700/{Git分支}/{服务名称}-{环境}.yml
1
2

微信截图_20221014140232.png

客户端

服务端配置完成之后,我们接着来配置一下客户端,那么现在我们的服务既然需要从服务器读取配置文件,那么就需要进行一些配置,我们删除原来的application.yml 文件(也可以保留,最后无论是远端配置还是本地配置都会被加载),改用bootstrap.yml(在application.yml之前加载,可以实现配置文件远程获取)

xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11

创建bootstrap.yml

yaml
spring:
  application:
    name: service-book
  cloud:
    config:
      # 名称,其实就是文件名称
      name: ${spring.application.name},common-datasource
      # 配置服务器的地址
      uri: http://localhost:8700
      # 环境
      profile: dev
      # 分支
      label: master
1
2
3
4
5
6
7
8
9
10
11
12
13

配置完成之后

shell
2022-10-14 14:12:20.125  INFO 25716 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:8700
2022-10-14 14:12:20.247  INFO 25716 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=service-book, profiles=[dev], label=master, version=507cadb00faf43ffe0a0356080de08dd77143126, state=null
2022-10-14 14:12:20.248  INFO 25716 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-configClient'}, BootstrapPropertySource {name='bootstrapProperties-file://D:/IdeaExampleProjects/hello-spring-cloud/config.repo/file:D:\IdeaExampleProjects\hello-spring-cloud\config.repo\service-book-dev.yml'}]
1
2
3

可以看到已经从远端获取到了配置,并进行启动。

原理

starter 会加载一个Configuration 里面加载了 一个ConfigServicePropertySourceLocator的Bean

java
public class ConfigServiceBootstrapConfiguration {
	@Bean
	@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
	@ConditionalOnProperty(name = ConfigClientProperties.PREFIX + ".enabled", matchIfMissing = true)
	public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
		return new ConfigServicePropertySourceLocator(properties);
	}
}
1
2
3
4
5
6
7
8

这个bean 就是去加载远程配置文件信息的一个类。

java
@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator{
    
    @Override
    @Retryable(interceptor = "configServerRetryInterceptor")
    public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
        ConfigClientProperties properties = this.defaultProperties.override(environment);

        if (StringUtils.startsWithIgnoreCase(properties.getName(), "application-")) {
            InvalidApplicationNameException exception = new InvalidApplicationNameException(properties.getName());
            if (properties.isFailFast()) {
                throw exception;
            }
            else {
                logger.warn(NAME_PLACEHOLDER + " resolved to " + properties.getName()
                        + ", not going to load remote properties. Ensure application name doesn't start with 'application-'");
                return null;
            }
        }

        CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService");
        ConfigClientRequestTemplateFactory requestTemplateFactory = new ConfigClientRequestTemplateFactory(logger,
                properties);

        Exception error = null;
        String errorBody = null;
        try {
            String[] labels = new String[] { "" };
            if (StringUtils.hasText(properties.getLabel())) {
                labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
            }
            String state = ConfigClientStateHolder.getState();
            // Try all the labels until one works
            for (String label : labels) {
                
                Environment result = getRemoteEnvironment(requestTemplateFactory, label.trim(), state);
                if (result != null) {
                    log(result);

                    // result.getPropertySources() can be null if using xml
                    if (result.getPropertySources() != null) {
                        for (PropertySource source : result.getPropertySources()) {
                            @SuppressWarnings("unchecked")
                            Map<String, Object> map = translateOrigins(source.getName(),
                                    (Map<String, Object>) source.getSource());
                            composite.addPropertySource(new OriginTrackedMapPropertySource(source.getName(), map));
                        }
                    }

                    HashMap<String, Object> map = new HashMap<>();
                    if (StringUtils.hasText(result.getState())) {
                        putValue(map, "config.client.state", result.getState());
                    }
                    if (StringUtils.hasText(result.getVersion())) {
                        putValue(map, "config.client.version", result.getVersion());
                    }
                    // the existence of this property source confirms a successful
                    // response from config server
                    composite.addFirstPropertySource(new MapPropertySource("configClient", map));
                    return composite;
                }
            }
            errorBody = String.format("None of labels %s found", Arrays.toString(labels));
        }
        catch (HttpServerErrorException e) {
            error = e;
            if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders().getContentType())) {
                errorBody = e.getResponseBodyAsString();
            }
        }
        catch (Exception e) {
            error = e;
        }
        if (properties.isFailFast()) {
            throw new IllegalStateException("Could not locate PropertySource and the fail fast property is set, failing"
                    + (errorBody == null ? "" : ": " + errorBody), error);
        }
        logger.warn("Could not locate PropertySource: " + (error != null ? error.getMessage() : errorBody));
        return null;

    }

    private Environment getRemoteEnvironment(ConfigClientRequestTemplateFactory requestTemplateFactory, String label,
                                             String state) {
        RestTemplate restTemplate = this.restTemplate == null ? requestTemplateFactory.create() : this.restTemplate;
        ConfigClientProperties properties = requestTemplateFactory.getProperties();
        String path = "/{name}/{profile}";
        String name = properties.getName();
        String profile = properties.getProfile();
        String token = properties.getToken();
        int noOfUrls = properties.getUri().length;
        if (noOfUrls > 1) {
            logger.info("Multiple Config Server Urls found listed.");
        }

        Object[] args = new String[] { name, profile };
        if (StringUtils.hasText(label)) {
            // workaround for Spring MVC matching / in paths
            label = Environment.denormalize(label);
            args = new String[] { name, profile, label };
            path = path + "/{label}";
        }
        ResponseEntity<Environment> response = null;
        List<MediaType> acceptHeader = Collections.singletonList(MediaType.parseMediaType(properties.getMediaType()));

        for (int i = 0; i < noOfUrls; i++) {
            Credentials credentials = properties.getCredentials(i);
            String uri = credentials.getUri();
            String username = credentials.getUsername();
            String password = credentials.getPassword();

            logger.info("Fetching config from server at : " + uri);

            try {
                HttpHeaders headers = new HttpHeaders();
                headers.setAccept(acceptHeader);
                requestTemplateFactory.addAuthorizationToken(headers, username, password);
                if (StringUtils.hasText(token)) {
                    headers.add(TOKEN_HEADER, token);
                }
                if (StringUtils.hasText(state) && properties.isSendState()) {
                    headers.add(STATE_HEADER, state);
                }

                final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
                response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args);
            }
            catch (HttpClientErrorException e) {
                if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
                    throw e;
                }
            }
            catch (ResourceAccessException e) {
                logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available");
                if (i == noOfUrls - 1) {
                    throw e;
                }
                else {
                    continue;
                }
            }

            if (response == null || response.getStatusCode() != HttpStatus.OK) {
                return null;
            }

            Environment result = response.getBody();
            return result;
        }

        return null;
    }
}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

注意中间有个方法 getRemoteEnvironment(requestTemplateFactory, label.trim(), state); 这里就是去加载git仓库的配置文件信息。 然后存放在HashMap里。

Released under the MIT License.