写在前面
本文参考之
Spring-Cloud-Config
官方文档。
学习微服务最好使用容器来搭建,对于正在学习编程的小伙伴推荐买上属于自己的一台服务器,用来练练手,也顺带学习 Docker,这很重要。最近,阿里在搞活动,新用户 1C2G 只要 98 一年,我也比较了很多,还是比较划算的,我自己也入手了,可以点进来看看,对了,最便宜的一款在 【全部必抢爆款】 里面: 阿里云服务器,助力云上云!
Spring启动应用程序可以立即利用Spring配置服务器(或应用程序开发人员提供的其他外部属性源)。它还获得了一些与 Environment
更改事件相关的其他有用特性。
1. 基于URI配置
Spring cloud 配置客户端在类路径上的应用程序的默认行为:当一个配置客户端启动时,它绑定到配置服务器,并使用远程属性源初始化 Spring Environment
。
所有希望使用配置服务器的客户端应用程序都需要 bootstrap.yml
(或者环境变量) ,通过 spring.cloud.config.uri
来设置服务器地址。该属性默认值是 “http://localhost:8888”。
配置看起来像这样:
spring:
application:
name: config-client
cloud:
config:
profile: prod
label: master
uri: http://localhost:60020/
name: duofei
username: duofei
password: duofei001
其中 profile
(环境)、label
(分支) 以及name
(应用程序名) 与前文所讲的配置服务端文件的定位有关。uri
指定配置服务器地址。用户名和密码是当服务器要求安全访问需要配置的。
2. 基于服务发现
如果使用服务发现的客户端,例如 eureka 或者 consul。如果想要使用 DiscoveryClient
来定位配置服务器,可以通过 spring.cloud.config.discovery.enabled=true
(默认为false)。即使这样做,客户端仍然需要一个具有适当配置的 bootstrap.yml
(或者环境变量)。
例如,使用Spring Cloud Netflix
,需要定义Eureka服务器地址(例如,在eureka.client.serviceurl. defaultzone
中)。使用此选项的代价是启动时额外的网络往返,以定位服务注册。这样做的好处是,只要发现服务是一个固定点,配置服务器就可以更改它的坐标。默认的服务ID是configserver
,但是您可以在客户端通过设置spring.cloud.config.discovery.serviceId
(在服务器上,以服务的通常方式,例如设置spring.application.name
)来更改它。
bootstrap.yml 看起来像这样:
spring:
application:
name: config-client
cloud:
config:
discovery:
enabled: true
service-id: config-server
profile: prod
label: master
name: duofei
server:
port: 60022
eureka:
client:
service-url:
defaultZone: http://localhost:1113/eureka/
config-server:
metadata-map:
password: duofei001
user: duofei
需要注意的是,如果基于配置服务器要求安全访问,那么,需要通过 eureka.client.config-server.metadata-map
来配置用户名和密码(不同于基于 URL 的配置)。
源码分析:
在我们通常的服务发现客户端程序中,需要在启动类上添加@EnableDiscoveryClient
注解,但在使用服务发现来完成配置时,我们并不需要这样做。查看源码,定位到 DiscoveryClientConfigServiceBootstrapConfiguration
。该类有以下注解:
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false)
@Configuration
@Import({ UtilAutoConfiguration.class })
@EnableDiscoveryClient
可以发现,当设置了 spring.cloud.config.discovery.enabled = true
时,将自动启用服务发现。
那么,如何确认在安全访问中需要在eureka.client.config-server.metadata-map
中配置 user
和 password
呢?依然是该类,查看 refresh()
方法:
private void refresh() {
try {
String serviceId = this.config.getDiscovery().getServiceId();
List<String> listOfUrls = new ArrayList<>();
List<ServiceInstance> serviceInstances = this.instanceProvider
.getConfigServerInstances(serviceId);
for (int i = 0; i < serviceInstances.size(); i++) {
ServiceInstance server = serviceInstances.get(i);
String url = getHomePage(server);
if (server.getMetadata().containsKey("password")) {
String user = server.getMetadata().get("user");
user = user == null ? "user" : user;
this.config.setUsername(user);
String password = server.getMetadata().get("password");
this.config.setPassword(password);
}
if (server.getMetadata().containsKey("configPath")) {
String path = server.getMetadata().get("configPath");
if (url.endsWith("/") && path.startsWith("/")) {
url = url.substring(0, url.length() - 1);
}
url = url + path;
}
listOfUrls.add(url);
}
String[] uri = new String[listOfUrls.size()];
uri = listOfUrls.toArray(uri);
this.config.setUri(uri);
}
catch (Exception ex) {
if (this.config.isFailFast()) {
throw ex;
}
else {
logger.warn("Could not locate configserver via discovery", ex);
}
}
}
可以发现,用户名密码会从服务实例的元数据Map
中解析。并且客户端并不是直接请求服务实例的url
来获取配置文件,而是将URL赋值给 config URL
(基于URI 配置)。所以,服务发现在这里的作用看起来更像一个提供配置类,提供 URL、用户名密码以及 config
路径(如果配置服务具有上下文,需要配置该值)。
Spring 这样的处理方式看起来非常合理,如果直接使用服务实例提供的 url 获取配置文件,那么看起来像拥有两种不同的方式请求配置文件。但服务实例如果仅仅是提供一些配置信息,那么仍然只有一种方式来获取请求配置文件(通过 URI 的方式),至少它们本来就是同一件事,无需重复,代码也无需重复。
3. 快速失败
在某些情况下,如果服务无法连接到配置服务器,则可能希望启动失败。如果这是需要的行为,那么设置引导配置属性spring.cloud.config.fail-fast=true
使客户端因为异常停机。
源码分析:
通过切换该属性值,观察服务启动情况;可以发现,当该属性值为 true时,在服务刚刚启动的阶段,我们就能获得状态异常,查看异常抛出的ConfigServicePropertySourceLocator
类,定位到locate
方法:
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null
? getSecureRestTemplate(properties) : this.restTemplate;
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(restTemplate, properties,
label.trim(), state);
if (result != null) {
log(result);
if (result.getPropertySources() != null) { // result.getPropertySources()
// can be null if using
// xml
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source
.getSource();
composite.addPropertySource(
new MapPropertySource(source.getName(), map));
}
}
if (StringUtils.hasText(result.getState())
|| StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(
new MapPropertySource("configClient", map));
}
return composite;
}
}
}
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: " + (errorBody == null
? error == null ? "label not found" : error.getMessage() : errorBody));
return null;
}
源码中,捕获的异常并未直接抛出,而是最后通过 properties.isFailFast()
判断是否抛出该异常,然后返回 null
。
4. 重试
如果您希望在应用程序启动时配置服务器偶尔不可用,您可以让它在失败后继续尝试。首先,需要设置spring.cloud.config.failure-fast=true
。然后需要将spring-retry
和spring-boot-start-aop
添加到类路径中。默认行为是重试6次,初始回退间隔为1000ms
,后续回退的指数乘数为1.1。您可以通过设置spring.cloud.config.retry.*
来配置这些属性(以及其他属性)。
要完全控制重试行为,可以使用
configServerRetryInterceptor
的ID添加RetryOperationsInterceptor
类型的@Bean
。Spring Retry有一个支持创建的RetryInterceptorBuilder
。
源码分析:
可以发现,在 ConfigServiceBootStrapConfiguration
中找到默认的 configServerRetryInterceptor
。
@ConditionalOnProperty("spring.cloud.config.fail-fast")
@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
@Configuration
@EnableRetry(proxyTargetClass = true)
@Import(AopAutoConfiguration.class)
@EnableConfigurationProperties(RetryProperties.class)
protected static class RetryConfiguration {
@Bean
@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
public RetryOperationsInterceptor configServerRetryInterceptor(
RetryProperties properties) {
return RetryInterceptorBuilder.stateless()
.backOffOptions(properties.getInitialInterval(),
properties.getMultiplier(), properties.getMaxInterval())
.maxAttempts(properties.getMaxAttempts()).build();
}
}
注意这里的 @EnableRetry(proxyTargetClass = true)
注解,该注解能够启用全局的@Retryable
注解。回到之前的 locate
方法,该方法拥有 @Retryable(interceptor = "configServerRetryInterceptor")
注解。
5. 定位远程配置资源
配置服务提供来自/{name}/{profile}/{label}
的属性源,其中客户端应用程序中的默认绑定如下:
- “name” =
${spring.application.name}
- “profile” =
${spring.profiles.active}
(actuallyEnvironment.getActiveProfiles()
) - “label” = “master”
你可以通过设置spring.cloud.config.*
来覆盖它们。(其中*
是name
、profile
或label
)。
6. 为配置服务器指定多个URL
要确保在部署了多个Config Server
实例并期望一个或多个实例不可用时具有高可用性,您可以指定多个 url
(在spring.cloud.config.uri
下以逗号分隔的列表的形式)。或将所有实例注册到类似Eureka
的服务注册表中(如果使用发现优先引导模式)。
注意,只有在配置服务器不运行时(即应用程序退出时)或连接超时时,这样做才能确保高可用性。例如,如果配置服务器返回500(内部服务器错误)响应,或者配置客户端从配置服务器接收到401(由于糟糕的凭证或其他原因),配置客户端不会尝试从其他
url
获取属性。这种错误表示用户问题,而不是可用性问题。
如果在配置服务器上使用HTTP基本安全机制,那么只有在将凭据嵌入到spring.cloud.config.uri
下指定的每个URL中时,才有可能支持每个配置服务器的验证凭据。如果使用任何其他类型的安全机制,则无法支持每个配置服务器身份验证和授权。
7. 超时配置
如果你想配置超时阈值:
spring.cloud.config.request-read-timeout
:阅读超时配置spring.cloud.config.request-connect-timeout
:响应超时配置
8. 安全访问
如果在服务器上使用HTTP基本安全性,客户端需要知道密码(如果不是默认的用户名)。您可以通过配置服务器URI或通过单独的用户名和密码属性指定用户名和密码,如下面的示例所示:
bootstrap.yml
spring:
cloud:
config:
uri: https://user:secret@myconfig.mycompany.com
或者:
spring:
cloud:
config:
uri: https://myconfig.mycompany.com
username: user
password: secret
9. 健康监测
配置客户端提供一个Spring引导健康指示器,该指示器尝试从配置服务器加载配置。可以通过设置health.config.enabled=false
来禁用健康指示器。由于性能原因,响应也被缓存。默认的缓存存活时间是5分钟。要更改该值,请设置health.config.time-to-live
属性(以毫秒为单位)。
10. 动态刷新
动态刷新功能需要客户端添加 spring-boot-starter-actuator
模块,并通过 management.endpoints.web.exposure.include
暴露 refresh
端点。当修改了配置文件值以后,通过 POST 请求访问暴露的 /refresh
端点,实现客户端配置内容的更新。
如果你觉得我的文章对你有所帮助,欢迎关注我的公众号。赞!
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!