Skip to content

18配置集成:如何访问配置中心中的配置信息?

在微服务系统中,各个业务微服务就是 Spring Cloud Config 配置服务器的客户端,也就可以通过它所提供的各种 HTTP 端点获取所需的配置信息。在今天的内容中,我们将从客户端角度出发关注于如何在业务服务中使用配置服务器中配置信息的方法,并详细介绍 Spring Cloud Config 的客户端工作原理。

访问配置中心中的配置项

要想获取配置服务器中的配置信息,我们首先需要初始化客户端,也就是在将各个业务微服务与 Spring Cloud Config 服务器端进行集成。初始化客户端的第一步是引入 Spring Cloud Config 的客户端组件 spring-cloud-config-client,如下所示。

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

然后我们需要在配置文件 application.yml中配置服务器的访问地址,如下所示:

xml
spring:
  application:
	name: userservice
	profiles:
    active:
      prod
	
  cloud:
    config:
	   enabled: true
	   uri: http://localhost:8888

以上配置信息中有几个地方值得注意。首先,这个 Spring Boot 应用程序的名称"userservice",该名称必须与上一课时中在配置服务器上创建的文件目录名称保持一致,如果两者不一致则访问配置信息会发生失败。其次,我们注意到 profile 值为 prod,意味着我们会使用生产环境的配置信息,也就是会获取配置服务器上 userservice-prod.yml 配置文件中的内容。最后,我们需要指定配置服务器所在的地址,也就是上面的 uri:http://localhost:8888

一旦我们引入了 Spring Cloud Config 的客户端组件,相当于在各个微服务中自动集成了访问配置服务器中 HTTP 端点的功能。也就是说,访问配置服务器的过程对于各个微服务而言是透明的,即微服务不需要考虑如何从远程服务器获取配置信息,而只需要考虑如何在 Spring Boot 应用程序中使用这些配置信息。接下来我们就来讨论使用这些配置信息的方法。

在现实的开发过程中,开发人员通常会创建各种自定义的配置信息。例如,在 SpringHealth 案例中,每台穿戴式设备上报健康数据的频率是一个可以提取的初始化参数。从系统扩展性上讲,这个频率是应该可以调整的,所以我们创建了一个自定义的配置项,如下所示:

xml
springhealth.device.datacollect.frequency = 10

这里我们设置了这个频率值为 10。那么,应用程序如何获取这个配置项的内容呢?通常有两种方法,一种是使用 @Value 注解注入配置信息,另一种则是使用 @ConfigurationProperties 注解。

使用 @Value 注解注入配置信息

使用 @Value 注解来注入配置项内容虽然是一种比较传统的实现方法,但我们仍然可以使用。针对前面给出的自定义配置项,我们可以构建一个 SpringHealthConfig 类并创建一个 frequency 字段,然后在该字段上添加 @Value 注解,并指向配置项的名称,如下所示:

java
@Component
public class SpringHealthConfig {
 
    @Value("${springhealth.device.datacollect.frequency}")
    private int frequency;
}

这个配置类背后复杂的远程 HTTP 端点的请求、配置参数的实例化等过程都由 @Value 注解自动完成。

使用 @ConfigurationProperties 注解注入配置信息

相较 @Value 注解,更为现代的一种做法是使用 @ConfigurationProperties 注解。在使用该注解时,往往会配套使用一个"prefix"属性,如下所示:

java
@Component
@ConfigurationProperties(prefix = "springhealth.device.datacollect")
public class SpringHealthConfig {
 
	private int frequency;
	 
	//省略 getter/setter
}

在上面的示例中,相当于把"springhealth.device.datacollect"这个 prefix 下的 frequency 配置项进行了加载。

现在,让我们考虑一种更复杂的场景。假设设备上传数据的频率并不是固定的,而是根据每个不同的设备会有不同的频率。那么如果使用 Yaml 格式来表示,现在的配置项内容就应该是这样:

xml
springhealth.device.datacollect.frequency:
	device1: 10
	device2: 20
	device3: 30

相比 @Value 注解只能用于指定具体某一个配置项,@ConfigurationProperties 可以用来批量提取配置内容。只要指定prefix,我们就可以把该 prefix 下的所有配置项按照名称自动注入业务代码中。如果想把上述配置项全部加载到业务代码中,我们可以直接在配置类 SpringHealthConfig 中定义一个 Map 对象,然后通过 Key-Value 对来保存这些配置数据,如下所示:

java
@Component
@ConfigurationProperties(prefix = "springhealth.device.datacollect")
public class SpringHealthConfig {
 
    private Map<String, Integer> frequencys = new HashMap<>();

	//省略 getter/setter
}

可以看到这里通过创建一个 HashMap 来保存这些 Key-Value 对。类似的,我们也可以实现常见的一些数据结构的自动嵌入。

整合数据库访问功能

在日常的开发过程中,配置文件的常见用途是存储各种外部工具的访问元数据,最典型的就是管理数据库连接配置。在我们的 SpringHealth 案例中,因为每个业务微服务都势必需要进行数据库操作,然后我们来演示如何通过 Spring Cloud Config 提供数据库访问数据源(Data Source)的配置过程。

我们以 user-service 为例来演示数据库访问功能,案例中使用的是JPA 和 MySQL,因此需要在服务中引入相关的依赖,如下所示。

xml
<dependency>
	   <groupId>org.springframework.boot</groupId>
	   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
	 
<dependency>
	   <groupId>mysql</groupId>
	   <artifactId>mysql-connector-java</artifactId>
</dependency>

我们首先定义 user-service 用到的 user 表结构和初始化数据,如下所示:

xml
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_code` varchar(20) DEFAULT NULL,
  `user_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO `user` VALUES ('1', 'user1', 'springhealth_user1');
INSERT INTO `user` VALUES ('2', 'user2', 'springhealth_user2');

然后我们在 user-service 中定义 User 的实体类,使用了 JPA 相关的 @Entity、@Table、@Id 和 @GeneratedValue 注解,如下所示:

java
@Entity
@Table(name = "user")
public class User {
 
    @Id
	@GeneratedValue
	private Long id;
    private String userCode;
	private String userName;
	 
	//省略 getter/setter
}

然后我们简单设计一个 UserRepository,该 Repository 继承了 CrudRepository 工具类并提供 findUserByUsername() 方法,如下所示:

java
@Repository
public interface UserRepository extends CrudRepository<User, Long>  {
 
    User findUserByUserName(String userName);
}

有了 UserRepository 之后,创建对应的 UserService 的和 UserController 的结构也非常简单,各个类的代码如下所示:

java
@Service
public class UserService {
 
    @Autowired
	private UserRepository userRepository;
	
	public User getUserByUserName(String userName) {

         return userRepository.findUserByUserName(userName);
	} 
}
	 
@RestController
@RequestMapping(value="users")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/{userName}", method = RequestMethod.GET)
    public User getUserByUserName(@PathVariable("userName") String userName) {
 
        User user = userService.getUserByUserName(userName);
        return user;
    } 
}

现在我们通过 Postman 访问http://localhost:8081/users/springhealth_user1 端点,可以获取对应数据库中的技术结果,表明数据库访问操作成功完成,位于配置服务器中的数据源配置信息已经生效。

通过上面的示例,我们可以看到在整合数据库访问功能的整个过程中,开发人员几乎不需要关注于背后所依赖的数据源配置信息就能实现数据库访问,基于 Spring Cloud Config 的配置中心解决方案屏蔽了配置信息存储和获取的实现复杂性。

Spring Cloud Config Client 工作机制

通过前面介绍的内容,我们明确了想要使用配置中心服务,只需在 Spring Boot 的配置文件中添加对服务器地址的引用即可。当然,前提是在类路径中添加对 Spring Cloud Config Client 的引用。那么,为什么只要添加了引用,就会在服务启动时自动获取远程的配置信息呢?这是今天我们需要回答的问题。在介绍 Spring Cloud Config Client 组件时,我们将采用反推的方法,即从获取服务器端配置信息的入口开始,逐步引出这个问题的答案。

远程访问配置信息

我们首先找到的是 ConfigServicePropertySourceLocator 类,因为我们在这个类中发现了一个 getRemoteEnvironment 方法。显然,作为客户端组件,Spring Cloud Config Client 的主要职责就是获取服务器端提供的配置信息。在上一课时中,我们已经知道在 Spring Cloud Config Server 中提供了一个 EnvironmentController 端点类来暴露配置信息,那么在客户端中势必存在一个入口来获取这些配置信息。这个入口就是 getRemoteEnvironment 方法,如下所示(部分内容做了裁剪):

java
private Environment getRemoteEnvironment(RestTemplate restTemplate, onfigClientProperties properties, String label, String state) {
        //根据服务器端点的 URL 准备参数
        String path = "/{name}/{profile}";
        String name = properties.getName();
        String profile = properties.getProfile();
        String token = properties.getToken();
        int noOfUrls = properties.getUri().length;

	    //处理 URL 中的"label"
        Object[] args = new String[] { name, profile };
        if (StringUtils.hasText(label)) {
            if (label.contains("/")) {
                label = label.replace("/", "(_)");
            }
            args = new String[] { name, profile, label };
            path = path + "/{label}";
        }
        ResponseEntity<Environment> response = null;
 
        for (int i = 0; i < noOfUrls; i++) {
            //准备用于安全访问的 Credentials 信息
            Credentials credentials = properties.getCredentials(i);
            String uri = credentials.getUri();
            String username = credentials.getUsername();
            String password = credentials.getPassword();
            try {
                HttpHeaders headers = new HttpHeaders();
                addAuthorizationToken(properties, headers, username, password);
                if (StringUtils.hasText(token)) {
                    headers.add(TOKEN_HEADER, token);
                }
                if (StringUtils.hasText(state) && properties.isSendState()) {
                    headers.add(STATE_HEADER, state);
                }
                headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
 
                //通过 RestTemplate 执行远程访问
                final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
                response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
                        Environment.class, args);
            }
            //这里省略 catch 处理和空值校验 
 
            Environment result = response.getBody();
            return result;
        }
 
        return null;
}

上述代码虽然看上去有点长,但如果我们对照 EnvironmentController 端点的实现方法,就很容易理解它的执行流程。上述代码的主要流程就是获取访问配置服务器所需的 application、profile、label 等参数,然后利用 RestTemplate 工具类执行 HTTP 请求。客户端从这个请求所返回的 Environment 对象中获得所需要的各项配置信息。

明白了获取远程配置信息的处理方式,我们来反推 getRemoteEnvironment 方法的触发过程。通过分析代码的调用链路,我们发现在 ConfigServicePropertySourceLocator 的 locate 方法中使用到了这个方法。而讲到这个方法就必须介绍 Spring Cloud 中的一个重要接口 PropertySourceLocator,ConfigServicePropertySourceLocator 就实现了这个接口。

PropertySourceLocator 与自动装配

在 Spring Cloud 中,PropertySourceLocator 接口定义如下,只包含前面提到的 locate 方法:

java
public interface PropertySourceLocator {
    PropertySource<?> locate(Environment environment);
}

当我们看到 PropertySourceLocator 接口的命名,以及结合服务启动时自动获取配置信息这一主题应该能够联想到,PropertySourceLocator 肯定被一个自动配置类所引用。我们在位于 PropertySourceLocator 的同一包结构中找到了 PropertySourceBootstrapConfiguration 类,该自动配置类中包含以下代码:

java
@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements                    ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
	 
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
	 
	public void setPropertySourceLocators(
            Collection<PropertySourceLocator> propertySourceLocators) {
this.propertySourceLocators = new ArrayList<>(propertySourceLocators);
    }
	 
	@Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        CompositePropertySource composite = new CompositePropertySource(
                BOOTSTRAP_PROPERTY_SOURCE_NAME);
        AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
        boolean empty = true;
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        for (PropertySourceLocator locator : this.propertySourceLocators) {
            PropertySource<?> source = null;

            //调用各个 PropertySourceLocator 的 locate 方法
            source = locator.locate(environment);
            if (source == null) {
                continue;
            }
            logger.info("Located property source: " + source);
            composite.addPropertySource(source);
            empty = false;
        }
        if (!empty) {
            MutablePropertySources propertySources = environment.getPropertySources();
            String logConfig = environment.resolvePlaceholders("${logging.config:}");
            LogFile logFile = LogFile.get(environment);
            if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
                propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
            }
            insertPropertySources(propertySources, composite);
            reinitializeLoggingSystem(environment, logConfig, logFile);
            setLogLevels(applicationContext, environment);
            handleIncludedProfiles(environment);
        }
    }
	
	//省略了其他变量和方法
}

显然,PropertySourceBootstrapConfiguration 实现了 ApplicationContextInitializer 接口中的 initialize 方法,而所有的 ApplicationContextInitializer 都会在 Spring Boot 应用程序启动时进行加载。这样,当类路径中引入了 Spring Cloud Config 之后,一个 ConfigServicePropertySourceLocator 实例就会被构建并保存在 PropertySourceBootstrapConfiguration 的 propertySourceLocators 数组中。然后,我们会遍历所有 propertySourceLocators 的 locate 方法,从而完成对远程服务配置信息的读取。

在 PropertySourceBootstrapConfiguration 类中,注意到 propertySourceLocators 数组是通过 setPropertySourceLocators 方法直接进行注入的,显然我们需要找到注入 ConfigServicePropertySourceLocator 的入口。

正如前文中我们通过 PropertySourceLocator 找到 PropertySourceBootstrapConfiguration 一样,在 ConfigServicePropertySourceLocator 类的同一个包结构中,我们也找到了 ConfigServiceBootstrapConfiguration 配置类,并在该类中发现了如下所示的 configServicePropertySource 方法:

java
@Bean   @ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
    @ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
    public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
        ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
                properties);
    return locator;
}

不难看出,上述方法创建了一个新的 ConfigServicePropertySourceLocator 实例。也就是说,当类路径中包含 ConfigServiceBootstrapConfiguration 类时,就会自动实例化一个 ConfigServicePropertySourceLocator。这里用到了 Spring Boot 的自动装配机制,我们通过查看 META-INF/spring.factories 中的配置类进行确认:

xml
# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration,\
	org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration

可以看到,BootstrapConfiguration 配置项中包含了 org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration 类的定义。

至此,围绕 Spring Cloud Config Client 如何在启动时自动获取 Server 所提供的配置信息的整体流程已经介绍完毕。作为总结,我们梳理这个过程中所涉及的核心类以及方法调用关系,如下图所示:

Spring Cloud Config 客户端访问服务端配置代码执行流程图(红色背景为客户端组件,绿色背景为服务端组件)

小结与预告

沿着上一课时的内容,本课时关注于如何使用 Spring Cloud Config Client 组件来访问位于配置服务器中的配置信息。我们通过引入 @Value 注解以及 @ConfigurationProperties 注解来实现了这一目标。同样的,我们发现使用这些注解非常简单方便,Spring Cloud Config 为我们自动屏蔽了所有内部的复杂实现逻辑。但对于你来说,还是结合本课时中给出的源码级的原理分析来深入背后的理解底层机制。

这里给你留一道思考题:为什么在类路径中添加了 Spring Cloud Config Client 组件之后,业务系统就能自动获取位于服务器端的配置信息呢?

在介绍完 Spring Cloud Config Client 组件之后,关于 Spring Cloud Config 我们还有一个核心的问题没有回答,即一旦位于配置服务器中的配置信息发生变更时,如何让各个客户端保持同步更新呢?这就是下一课时需要讨论的内容。