SpringCloudConfig之客户端详解




写在前面

本文参考之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 中配置 userpassword 呢?依然是该类,查看 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-retryspring-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} (actually Environment.getActiveProfiles())
  • “label” = “master”

你可以通过设置spring.cloud.config.*来覆盖它们。(其中*nameprofilelabel)。


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 端点,实现客户端配置内容的更新。

如果你觉得我的文章对你有所帮助,欢迎关注我的公众号。赞!我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值