SpringCloud源码之配置中心Config

338 阅读6分钟

一、什么是SpringCloud Config:

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以也面临了一些问题:
随着微服务工程的越来越多,每个工程都有一套配置文件,系统膨胀;
如果每个项目都有公共的比如数据库链接信息,没有统一的管理,想要修改则需要每个工程都要修改;
我们通常有很多系统环境:如prod(生产环境)、test(测试环境)、dev(预发布环境)
........
所以一套集中式的、动态的配置管理设施是必不可少的。SpringCloud Config就是这样的一个分布式配置中心。
Spring Cloud Config为分布式系统中的外部化配置提供服务器端和客户端支持。在服务端提供配置信息的存储,并提供对外访问的接口。配置信息的存储可以有多种方式,如数据库DB存储,git和SVN存储,文件存储等等。客户端在系统启动时调用服务端的接口读取到配置信息并缓存在本地,系统中使用@Value注解的就能读取到配置信息。

二、Demo:

1、Server端:

1)、引入依赖:

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

2)、开启配置中心开关:

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

通过@EnableConfigServer注解打开配置中心开关。

3)、配置文件application.properties:

spring.application.name=config
spring.cloud.config.server.prefix=/config

server.port=8810

spring.profiles.active=jdbc
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/config?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=*******
spring.cloud.config.server.jdbc.sql=SELECT properties_key, properties_value FROM properties WHERE application=? AND profile=? AND label=?

从上面的配置可以看到,此处的配置信息存储采用的是数据库存储,因此为了存储配置信息,这里建了两张数据表:

CREATE TABLE `properties_file` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `application` varchar(256) NOT NULL COMMENT '应用名',
  `profile` varchar(256) NOT NULL COMMENT 'profile 取值 dev/test/mo/prod',
  `label` varchar(256) NOT NULL COMMENT '标签 目前没有使用 默认master',
  `content` text NOT NULL COMMENT '配置文件内容',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `create_user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '创建用户',
  `last_update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `last_update_user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '修改用户',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='配置文件表';

CREATE TABLE `properties` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `properties_file_id` int(11) NOT NULL COMMENT '原始配置文件id',
  `application` varchar(256) NOT NULL COMMENT '应用名',
  `profile` varchar(256) NOT NULL COMMENT 'profile 取值 dev/test/mo/prod',
  `label` varchar(256) NOT NULL COMMENT '标签 目前没有使用 默认master',
  `properties_key` varchar(256) NOT NULL COMMENT '配置项',
  `properties_value` varchar(2048) NOT NULL COMMENT '配置值',
  `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `create_user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '创建用户',
  `last_update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `last_update_user_id` varchar(32) NOT NULL DEFAULT '' COMMENT '修改用户',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='配置信息表';

2、Client端:

1)、引入依赖:

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

2)、配置文件bootstrap.properties

spring.cloud.config.uri=http://localhost:8810/config

spring.cloud.config.name=xxxx
spring.cloud.config.profile=dev
spring.cloud.config.label=xxx
spring.cloud.config.fail-fast=true

3)、使用:

在代码中编写如下代码:

@Value("${test.value}")
private String testValue;

我们在配置中心配置上test.value的属性,这样虽然在项目本地配置文件中虽然没有配置这个属性,但是代码依然能正确读取到test.value的属性值。

三、Server端源码分析:

由于Server端的功能是提供配置信息的存储和查询,而配置信息的存储又支持多种方式:JDBC, Git, SVN, Redis, AWS等等,因此需要针对不同存储的实现方式的查询提供一个统一的接口,这样对外的查询接口就可以以一种统一的方式进行调用。这个统一的接口是EnvironmentRepository,其定义如下:

public interface EnvironmentRepository {
	Environment findOne(String application, String profile, String label);

	default Environment findOne(String application, String profile, String label,
			boolean includeOrigin) {
		return findOne(application, profile, label);
	}
}

然后针对每一种实现方式都有一个实现的bean,并根据配置spring.profiles.active进行bean的加载和注入(比如我们这个Demo配置的jdbc,因此加载和注入的bean就是JdbcEnvironmentRepository)。

1、相关bean的注入:

1)、EnableConfigServer注解引入ConfigServerConfiguration,ConfigServerConfiguration定义了一个名为Marker的bean。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {

}

@Configuration(proxyBeanMethods = false)
public class ConfigServerConfiguration {
	@Bean
	public Marker enableConfigServerMarker() {
		return new Marker();
	}

	class Marker {
	}
}

2)、Spring.factories文件指定了EnableAutoConfiguration=ConfigServerAutoConfiguration,ConfigServerAutoConfiguration的注入依赖于条件Marker。

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class,
		ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class,
		ConfigServerMvcConfiguration.class, ResourceEncryptorConfiguration.class })
public class ConfigServerAutoConfiguration {

}

可以看到ConfigServerAutoConfiguration加了依赖条件@ConditionalOnBean(Marker.class),同时也import进了很多Configuration,这里我们看看EnvironmentRepositoryConfiguration的定义:

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ SvnKitEnvironmentProperties.class,
		CredhubEnvironmentProperties.class, JdbcEnvironmentProperties.class,
		NativeEnvironmentProperties.class, VaultEnvironmentProperties.class,
		RedisEnvironmentProperties.class, AwsS3EnvironmentProperties.class })
@Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class,
		VaultConfiguration.class, VaultRepositoryConfiguration.class,
		SpringVaultRepositoryConfiguration.class, CredhubConfiguration.class,
		CredhubRepositoryConfiguration.class, SvnRepositoryConfiguration.class,
		NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class,
		RedisRepositoryConfiguration.class, GoogleCloudSourceConfiguration.class,
		AwsS3RepositoryConfiguration.class, DefaultRepositoryConfiguration.class })
public class EnvironmentRepositoryConfiguration {
	............
	
	@Configuration(proxyBeanMethods = false)
    @Profile("jdbc")
    @ConditionalOnClass(JdbcTemplate.class)
    class JdbcRepositoryConfiguration {

        @Bean
        @ConditionalOnBean(JdbcTemplate.class)
        public JdbcEnvironmentRepository jdbcEnvironmentRepository(
                JdbcEnvironmentRepositoryFactory factory,
                JdbcEnvironmentProperties environmentProperties) {
            return factory.build(environmentProperties);
        }

    }
	............
}

其中jdbc相关的配置属性都在JdbcEnvironmentProperties类中指定了。

JdbcRepositoryConfiguration配置类通过@Profile("jdbc")指定只有在指定spring.profiles.active=jdbc时才会注入这个bean。

JdbcEnvironmentRepository类是通过工厂类JdbcEnvironmentRepositoryFactory来实例化的:

public class JdbcEnvironmentRepositoryFactory implements EnvironmentRepositoryFactory<JdbcEnvironmentRepository, JdbcEnvironmentProperties> {

	private JdbcTemplate jdbc;

	public JdbcEnvironmentRepositoryFactory(JdbcTemplate jdbc) {
		this.jdbc = jdbc;
	}

	@Override
	public JdbcEnvironmentRepository build(
			JdbcEnvironmentProperties environmentProperties) {
		return new JdbcEnvironmentRepository(this.jdbc, environmentProperties);
	}
}

接下来看看JdbcEnvironmentRepository是如何实现基于数据库的查询的。

2、JDBC方式的配置信息的存储和查询:

public class JdbcEnvironmentRepository implements EnvironmentRepository, Ordered {
	private final JdbcTemplate jdbc;

	private final PropertiesResultSetExtractor extractor = new PropertiesResultSetExtractor();

	private int order;

	private String sql;

	@Override
	public Environment findOne(String application, String profile, String label) {
		String config = application;
		if (StringUtils.isEmpty(label)) {
			label = "master";
		}
		if (StringUtils.isEmpty(profile)) {
			profile = "default";
		}
		//profile增加一个公共的查询default
		if (!profile.startsWith("default")) {
			profile = "default," + profile;
		}
		String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
		Environment environment = new Environment(application, profiles, label, null,null);
		//application增加一个公共的查询application
		if (!config.startsWith("application")) {
			config = "application," + config;
		}
		List<String> applications = new ArrayList<String>(new LinkedHashSet<>(Arrays.asList(StringUtils.commaDelimitedListToStringArray(config))));
		List<String> envs = new ArrayList<String>(new LinkedHashSet<>(Arrays.asList(profiles)));		
		for (String app : applications) {
			for (String env : envs) {
				//通过数据库查询指定条件的配置信息列表
				Map<String, String> next = (Map<String, String>) this.jdbc.query(this.sql,new Object[] { app, env, label }, this.extractor);
				if (!next.isEmpty()) {
					environment.add(new PropertySource(app + "-" + env, next));
				}
			}
		}
		return environment;
	}
}

这里的数据库查询,就是执行的我们在配置文件中指定的配置spring.cloud.config.server.jdbc.sql=SELECT properties_key, properties_value FROM properties WHERE application=? AND profile=? AND label=?

3、提供查询接口:

提供查询的接口实现在EnvironmentController中:

@RestController
@RequestMapping(method = RequestMethod.GET,
		path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {

	private EnvironmentRepository repository;
	............
	
	@RequestMapping(path = "/{name}/{profiles:.*[^-].*}",
			produces = MediaType.APPLICATION_JSON_VALUE)
	public Environment defaultLabel(@PathVariable String name,
			@PathVariable String profiles) {
		return getEnvironment(name, profiles, null, false);
	}
	
	public Environment getEnvironment(String name, String profiles, String label,
			boolean includeOrigin) {
		name = normalize(name);
		label = normalize(label);
		Environment environment = this.repository.findOne(name, profiles, label, includeOrigin);		
		return environment;
	}
	
	............
}

可以看到EnvironmentController的查询其实就是调用的EnvironmentRepository的接口查询的。

四、Client端源码分析:

Client端的任务就是调用Server端的接口获取配置属性数据并添加到Environment 的PropertySource中,这样代码中的配置属性读取就能到Environment 中读取。由于配置属性可能不止除了读取配置中心一种方式,为了支持更多其他形式的配置属性读取,因此设计了一个统一的接口PropertySourceLocator来读取,这个接口只提供了一个Locate方法,代码如下:

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

	default Collection<PropertySource<?>> locateCollection(Environment environment) {
		return locateCollection(this, environment);
	}

	static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator, Environment environment) {
		PropertySource<?> propertySource = locator.locate(environment);
		return Arrays.asList(propertySource);		
	}
}

可以看到PropertySourceLocator接口中的locateCollection方法提供了统一读取配置属性的代码框架,调用locator.locate(environment)去具体读取配置属性,而locator.locate又是交由实现类去实现的。

1、相关bean的注入:

1)、在应用程序启动SpringApplication.run时会调用prepareContext来完成刷新Context之前的预处理工作;

2)、prepareContext调用applyInitializers来循环调用所有实现了ApplicationContextInitializer接口的bean;

3)、其中SpringCloud Config定义了一个类PropertySourceBootstrapConfiguration实现了ApplicationContextInitializer接口,这个bean会遍历所有实现了PropertySourceLocator接口的bean并分别调用;

4)、其中ConfigServicePropertySourceLocator实现了PropertySourceLocator接口并完成了从配置中心读取配置属性。

public ConfigurableApplicationContext run(String... args) {
	//.......省略部分代码,只保留核心代码..............
	context = createApplicationContext();
	//重点,正是在这里完成配置中心属性读取的
	prepareContext(context, environment, listeners, applicationArguments, printedBanner);
	refreshContext(context);
	afterRefresh(context, applicationArguments);
		
	return context;
}

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
		SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	//重点,这里调用ApplicationContextInitializer
	applyInitializers(context);
	listeners.contextPrepared(context);
	//..........省略部分代码,只保留核心代码.............
}

protected void applyInitializers(ConfigurableApplicationContext context) {
	for (ApplicationContextInitializer initializer : getInitializers()) {		
		initializer.initialize(context);
	}
}

public void initialize(ConfigurableApplicationContext applicationContext) {
	//.........省略部分代码..........
	List<PropertySource<?>> composite = new ArrayList<>();	
	boolean empty = true;	
	for (PropertySourceLocator locator : this.propertySourceLocators) {
		//这里调用PropertySourceLocator查找配置中心上的配置信息
		Collection<PropertySource<?>> source = locator.locateCollection(environment);				
		for (PropertySource<?> p : source) {
			composite.add(new SimpleBootstrapPropertySource(p));
		}
		empty = false;
	}
	if (!empty) {
		MutablePropertySources propertySources = environment.getPropertySources();
		//将配置中心读取到的配置信息添加到本地environment中去
		insertPropertySources(propertySources, composite);
		//.........省略部分代码..........
	}
}

a、this.propertySourceLocators包含了所有实现了PropertySourceLocator接口的集合,从其定义可以看出是通过Autowired注入的,而ConfigServicePropertySourceLocator的实例化又是在ConfigServiceBootstrapConfiguration中定义的:

//PropertySourceBootstrapConfiguration.java

@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

//ConfigServiceBootstrapConfiguration.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;
}

b、locator.locateCollection通过调用ConfigServicePropertySourceLocator.locate方法来读取配置中心上的配置信息。

2、读取配置中心的源码:

从上面的PropertySourceLocator接口的locateCollection方法可以看出最终还是调用ConfigServicePropertySourceLocator.locate方法来读取配置中心上的配置信息的,实现代码如下:

public PropertySource<?> locate(Environment environment) {
	CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService");
	RestTemplate restTemplate = this.restTemplate;
	String[] labels = new String[] { "" };
	for (String label : labels) {
		//从远程服务器上读取指定的配置属性集合
		Environment result = getRemoteEnvironment(restTemplate, properties, label.trim(), state);
		for (PropertySource source : result.getPropertySources()) {
			//将读取到的配置属性转换成key-value形式到map中
			Map<String, Object> map = translateOrigins(source.getName(), (Map<String, Object>) source.getSource());
			composite.addPropertySource(new OriginTrackedMapPropertySource(source.getName(), map));
		}			
		return composite;
	}
	
	return null;
}

private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties, String label, String state) {
	//读取相关属性信息
	String path = "/{name}/{profile}";
	String name = properties.getName();
	String profile = properties.getProfile();
	String token = properties.getToken();
	int noOfUrls = properties.getUri().length;	

	//组件url路径
	Object[] args = new String[] { name, profile };
	if (StringUtils.hasText(label)) {
		label = Environment.denormalize(label);
		args = new String[] { name, profile, label };
		path = path + "/{label}";
	}	

	for (int i = 0; i < noOfUrls; i++) {
		Credentials credentials = properties.getCredentials(i);
		//uri就是在bootstrap.properties中定义的spring.cloud.config.uri=http://localhost:8810/config
		String uri = credentials.getUri();
		String username = credentials.getUsername();
		String password = credentials.getPassword();		

		//设置请求头信息
		HttpHeaders headers = new HttpHeaders();
		headers.setAccept(Collections.singletonList(MediaType.parseMediaType(V2_JSON)));
		addAuthorizationToken(properties, headers, username, password);
		headers.add(TOKEN_HEADER, token);
		headers.add(STATE_HEADER, state);

		final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
		//调用http接口请求配置中心的配置信息
		response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args);

		return response.getBody();		
	}

	return null;
}

可以看到读取配置属性信息还是调用的http://localhost:8810/config/{name}/{profile}/{label}来读取的。读取到之后添加到environment.propertuSources中去,并且从远程配置中心读取到的配置信息存储在一个名为”configService“的PropertySource中。