spring cloud config基本使用
spring cloud config是一个基于http协议的远程配置实现方式。通过统一的配置管理服务器进行配置管理,客户端通过https协议主动的拉取服务的的配置信息,完成配置获取。
spring cloud config的使用方式非常简单,spring cloud config server默认的实现方式是git管理配置,官方文档介绍已经详细说明有几种使用方式。下面看一下git的spring cloud config server实现方式。
spring cloud config server使用
SpringApplication实现,代码如下。
@SpringBootApplication
@EnableConfigServer
public class SpringCloudConfigServer {
public static void main(String[] args) {
SpringApplication.run(SpringCloudConfigServer.class, args);
}
}
application.properties配置文件,代码如下。
spring.application.name=configServer
server.port=8888
#配置获取地址
spring.cloud.config.server.git.uri=
${user.dir}/all-about-learn/garine-learn-spring-clound/garine-learn-config-server/src/main/resources/configs/
最简单的配置方式就是配置以上几项,同时,需要把spring.cloud.config.server.git.uri配置的git repo创建起来,里面的配置文件也需要创建。
这里使用的是本地git repo,进入到spring.cloud.config.server.git.uri配置的目录,git bash执行git init命令创建repo,同时创建一个配置文件config.properties,表示应用名config的应用默认的请求配置文件内容。如果命名是config-dev.properties表示应用名config的应用开发环境的请求配置文件内容。config.properties必须提交到git repo才能被读取到。
启动程序,访问http://localhost:8888/config/default可以读取到config.properties的配置内容。
请求配置的参数通过路径参数设置。
例如:http://localhost:8888/{应用名}/{profile}/{label}
{label分支,不传的话默认master
spring cloud config client使用
先进行bootstrap.properties文件配置如下。
# 和git里的文件名对应
spring.application.name=config
# 远程仓库的分支
spring.cloud.config.label=master
# dev 开发环境配置文件 | test 测试环境 | pro 正式环境
# 和git里的文件名对应
#spring.cloud.config.profile=default
# 指明配置服务中心的网址
spring.cloud.config.uri= http://localhost:8888/
server.port=8080
然后就是SpringApplication实现,尝试读取一个配置属性。
@SpringBootApplication
@RestController
public class SpringCloudConfigClient {
public static void main(String[] args) {
SpringApplication.run(SpringCloudConfigClient.class, args);
}
@Value("${name}")
String name;
@RequestMapping("/getName")
public String getName(){
return name;
}
}
访问地址,返回config.properties里面的配置属性garine.
spring cloud config server 实现原理分析
@EnableConfigServer
首先,查看@EnableConfigServer注解,Enable注解编程模型通常都是引入某种Configuration类来达到装配某些bean的目的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {
}
ConfigServerConfiguration
@Configuration
public class ConfigServerConfiguration {
class Marker {}
@Bean
public Marker enableConfigServerMarker() {
return new Marker();
}
}
ConfigServerConfiguration类里面并没有实现太多bean的装配,这里利用一种折中方式,引入需要的自动配置。请看下面的类。Marker唯一被引用的地方在ConfigServerAutoConfiguration类
ConfigServerAutoConfiguration
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)表示当装配了ConfigServerConfiguration.Marker的实例时才会执行ConfigServerAutoConfiguration的处理。这里又另外引入了5个配置类。分析config server,重点看EnvironmentRepositoryConfiguration类。
EnvironmentRepositoryConfiguration
@Configuration
@EnableConfigurationProperties({ SvnKitEnvironmentProperties.class,
JdbcEnvironmentProperties.class, NativeEnvironmentProperties.class, VaultEnvironmentProperties.class })
@Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class, VaultRepositoryConfiguration.class,
SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class,
DefaultRepositoryConfiguration.class })
public class EnvironmentRepositoryConfiguration {
}
这里的@Import又引入了7种配置类,查看文档会发现其实刚好对应config server的几种实现方式git的实现方式使用的配置类就是GitRepositoryConfiguration。以GitRepositoryConfiguration的为例分析。
GitRepositoryConfiguration
@Configuration
@Profile("git")
class GitRepositoryConfiguration extends DefaultRepositoryConfiguration {
}
可以看出,GitRepositoryConfiguration其实是默认的实现方式,查看DefaultRepositoryConfiguration的代码。
@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private ConfigServerProperties server;
@Autowired(required = false)
private TransportConfigCallback customTransportConfigCallback;
@Bean
public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
return gitEnvironmentRepositoryFactory.build(environmentProperties);
}
}
最终是装配一个MultipleJGitEnvironmentRepository的bean,实际每种配置类的实现的最终都是装配一个EnvironmentRepository的子类,可以认为,有一个地方最终会引用到EnvironmentRepository的bean,使用org.springframework.cloud.config.server.environment.EnvironmentRepository#findOne方法来查询配置。
EnvironmentController
尝试搜索使用到findOne方法的类,org.springframework.cloud.config.server.environment.EnvironmentController#labelled中使用到,而且这里面是创建了一个RestController,推测应该是客户端获取服务端配置的入口,查看代码如下。
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
if (name != null && name.contains("(_)")) {
// "(_)" is uncommon in a git repo name, but "/" cannot be matched
// by Spring MVC
name = name.replace("(_)", "/");
}
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
Environment environment = this.repository.findOne(name, profiles, label);
if(!acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())){
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
注意这里的EnvironmentController#repository属性就是GitRepositoryConfiguration实例化的MultipleJGitEnvironmentRepository,如果是别的实现方式就是别的EnvironmentRepository。可以看出”/{name}/{profiles}/{label:.*}”路径参数正好与我们的请求方式相对应,因此Config Server是通过建立一个RestController来接收读取配置请求的,然后使用EnvironmentRepository来进行配置查询,返回一个org.springframework.cloud.config.environment.Environment对象的json串,推测客户端接收时也应该是反序列化为org.springframework.cloud.config.environment.Environment的一个实例。可以看一下Environment的属性定义。
private String name;
private String[] profiles = new String[0];
private String label;
private List<PropertySource> propertySources = new ArrayList<>();
private String version;
private String state;
尝试自定义EnvironmentRepository 实现
在上面的分析可以知道,所有的配置EnvironmentRepository的Configuration都是在没有EnvironmentRepository的bean的时候才会生效,我们可以实现自定义的EnvironmentRepository的bean,然后就可以覆盖的系统的实现。代码如下。
@SpringBootApplication
@EnableConfigServer
public class SpringCloudDefineConfigServer {
public static void main(String[] args) {
SpringApplication.run(SpringCloudDefineConfigServer.class, args);
}
@Bean
public EnvironmentRepository newEnvironmentRepository(){
return new EnvironmentRepository() {
@Override
public Environment findOne(String application, String profile, String label) {
Environment environment =new Environment(application, profile);
List<PropertySource> propertySourceList = environment.getPropertySources();
Map<String, String> map = new HashMap<>();
map.put("name", "garine-define");
PropertySource propertySource = new PropertySource("map", map);
propertySourceList.add(propertySource);
return environment;
}
};
}
}
这样就可以自定义获取配置方式。
spring cloud config client实现原理分析
getRemoteEnvironment
前面说到,配置服务器rest接口返回的是Environment的json串,那么client这边反序列化应该也是Environment,搜索spring-cloud-config-client包使用Environment的地方,发现这个方法。
org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#getRemoteEnvironment,目测就是获取远程服务器配置的地方。代码如下。
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;
if (noOfUrls > 1) {
logger.info("Multiple Config Server Urls found listed.");
}
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 = 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();
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);
}
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;
}
上面的代码主要操作就是拼接一个请求配置地址串,获取所需的ApplicationName,profile,label参数,利用RestTemplate执行http请求,返回的json反序列化为Environment,从而获得所需要的配置信息。
那么问题来了,client是在什么时候调用getRemoteEnvironment方法的,推测应该是在boostrap context进行初始化阶段。在getRemoteEnvironment打个断点,重启client程序,可以查看到以下调用链路。
- org.springframework.boot.SpringApplication#run(java.lang.String…)
- org.springframework.boot.SpringApplication#prepareContext
- org.springframework.boot.SpringApplication#applyInitializers
- org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#initialize
- org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#locate
- org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#getRemoteEnvironment
所以,可以知道在spring启动时就会远程加载配置信息,SpringApplication#applyInitializers代码如下,会遍历所有initializer进行一遍操作,PropertySourceBootstrapConfiguration就是其中之一的initializer。
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
当引入了spring-cloud-config后PropertySourceBootstrapConfiguration#propertySourceLocators中会新增一个ConfigServicePropertySourceLocator实例。在PropertySourceBootstrapConfiguration#initialize中遍历propertySourceLocators的locate方法,然后读取远程服务配置信息;如果没有引入了spring-cloud-config,那么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;
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#propertySourceLocators初始化
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
上面的代码可以看出,这里的propertySourceLocators是直接注入上下文中管理的PropertySourceLocator实例,所以PropertySourceLocator一定有别的地方初始化。
搜索ConfigServicePropertySourceLocator的使用处,发现org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration#configServicePropertySource方法装配了一个ConfigServicePropertySourceLocator的bean,代码如下。
@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
properties);
return locator;
}
//........
}
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration是config client的类,当引入了spring cloud config时引入,再尝试搜索使用处,发现在spring cloud config client包里面的spring.factories里面引入了ConfigServiceBootstrapConfiguration,熟悉spring boot自动装配的都知道,程序会自动加载spring.factories里面的配置类。
也就是说,当引入了spring cloud config client包,就会自动加载ConfigServiceBootstrapConfiguration类,自动装配ConfigServiceBootstrapConfiguration里面配置的bean,也就自动实例化一个ConfigServicePropertySourceLocator。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.cloud.config.client.ConfigClientAutoConfiguration
# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration, org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration