基于spring-cloud-config-server-2.0.2.RELEASE代码
spring cloud config的主函数是ConfigServerApplication
,其定义如下:
package com.liuwen;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args){
SpringApplication.run(ConfigServerApplication.class,args);
}
}
@EnableConfigServer是spring cloud定义的注解,
@EnableConfigServer定义如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ConfigServerConfiguration.class})
public @interface EnableConfigServer {
}
可以看出,它引入了ConfigServerConfiguration
@Configuration
public class ConfigServerConfiguration {
public ConfigServerConfiguration() {
}
@Bean
public ConfigServerConfiguration.Marker enableConfigServerMarker() {
return new ConfigServerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
}
ConfigServerConfiguration类里面并没有实现太多bean的装配,这里利用一种折中方式,引入需要的自动配置。请看下面的类。Marker唯一被引用的地方在ConfigServerAutoConfiguration类。
ConfigServerConfiguration
装配了一个Marker
的Bean。这个bean则有开启了ConfigServerAutoConfiguration
。
@Configuration
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({ConfigServerProperties.class})
@Import({EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class})
public class ConfigServerAutoConfiguration {
public ConfigServerAutoConfiguration() {
}
}
这里又引入了多个配置类,包括:
EnvironmentRepositoryConfiguration
CompositeConfiguration
ResourceRepositoryConfiguration
ConfigServerEncryptionConfiguration
ConfigServerMvcConfiguration
TransportConfiguration
接下来介绍EnvironmentRepositoryConfiguration
EnvironmentRepositoryConfiguration是配置中心的关键Configuration类。这个配置类中包含很多实现了EnvironmentRepository接口的类,每个实现类都对应一种类型(git/svn/navtie/vault)的配置。 EnvironmentRepositoryConfiguration通过profile注解(对当前应用的环境)决定使用装配哪个EnvironmentRepository Bean。默认是MultipleJGitEnvironmentRepository
(在内部类DefaultRepositoryConfiguration
里面有)。
@Configuration
public class EnvironmentRepositoryConfiguration {
@Bean
@ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true)
public ConfigServerHealthIndicator configServerHealthIndicator(EnvironmentRepository repository) {
return new ConfigServerHealthIndicator(repository);
}
@Configuration
@ConditionalOnMissingBean(EnvironmentRepository.class)
protected static class DefaultRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private ConfigServerProperties server;
@Autowired(required = false)
private TransportConfigCallback transportConfigCallback;
@Bean
public MultipleJGitEnvironmentRepository defaultEnvironmentRepository() {
MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository(this.environment);
repository.setTransportConfigCallback(this.transportConfigCallback);
if (this.server.getDefaultLabel()!=null) {
repository.setDefaultLabel(this.server.getDefaultLabel());
}
return repository;
}
}
@Configuration
@Profile("native")
protected static class NativeRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Bean
public NativeEnvironmentRepository nativeEnvironmentRepository() {
return new NativeEnvironmentRepository(this.environment);
}
}
@Configuration
@Profile("git")
protected static class GitRepositoryConfiguration extends DefaultRepositoryConfiguration {}
@Configuration
@Profile("subversion")
protected static class SvnRepositoryConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private ConfigServerProperties server;
@Bean
public SvnKitEnvironmentRepository svnKitEnvironmentRepository() {
SvnKitEnvironmentRepository repository = new SvnKitEnvironmentRepository(this.environment);
if (this.server.getDefaultLabel()!=null) {
repository.setDefaultLabel(this.server.getDefaultLabel());
}
return repository;
}
}
@Configuration
@Profile("vault")
protected static class VaultConfiguration {
@Bean
public VaultEnvironmentRepository vaultEnvironmentRepository(HttpServletRequest request, EnvironmentWatch watch) {
return new VaultEnvironmentRepository(request, watch, new RestTemplate());
}
}
@Configuration
@ConditionalOnProperty(value = "spring.cloud.config.server.consul.watch.enabled")
protected static class ConsulEnvironmentWatchConfiguration {
@Bean
public EnvironmentWatch environmentWatch() {
return new ConsulEnvironmentWatch();
}
}
@Configuration
@ConditionalOnMissingBean(EnvironmentWatch.class)
protected static class DefaultEnvironmentWatch {
@Bean
public EnvironmentWatch environmentWatch() {
return new EnvironmentWatch.Default();
}
}
}
EnvironmentRepository
是一个配置管理仓库接口,抽象了获取配置的方法:
Environment findOne(String application, String profile, String label);
从名字中大概可以看出,这些类应该是用于加载不同类型的配置(后面会再介绍)。
有了获取配置的类,还差对外提供接口的类,就是EnvironmentController
EnvironmentController
是spring-cloud-config-server
包的一个controller,其他服务一般是通过这个controller获取相应配置。
@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
private EnvironmentRepository repository;
private ObjectMapper objectMapper;
public EnvironmentController(EnvironmentRepository repository,
ObjectMapper objectMapper) {
this.repository = repository;
this.objectMapper = objectMapper;
}
// 获取配置的接口
...
}
它的关键成员变量有两个:
一般情况Spring为EnvironmentController注入的类是EnvironmentEncryptorEnvironmentRepository。
ObjectMapper用于当请求json格式的配置时的序列化。
EnvironmentController提供了多种获取配置的方法,这些方法主要接受application profile label这三个(或者更少)的参数,这三个参数的具体含义可以参考官网的说明,下面列举了部分方法:
@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
@PathVariable String profiles) {
return labelled(name, profiles, null);
}
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
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);
return environment;
}
我们访问http://localhost:8081/config/mysql/dev(这是作者的配置,每个人可能不一样), 进入defaultLabel方法,它会再调用labelled方法(由于没有制定label参数,所以label传了个null)。
@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
@PathVariable String profiles) {
return labelled(name, profiles, null);
}
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
// 调用`EnvironmentRepository`的findOne方法返回对应的配置
Environment environment = this.repository.findOne(name, profiles, label);
return environment;
}
在labelled
方法中,会调用EnvironmentRepository
的findOne()来加载配置,然后返回给配置获取方。
前面提到spring config
通过EnvironmentEncryptorEnvironmentRepository
加载配置
public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRepository {
private EnvironmentRepository delegate;
private EnvironmentEncryptor environmentEncryptor;
public EnvironmentEncryptorEnvironmentRepository(EnvironmentRepository delegate,
EnvironmentEncryptor environmentEncryptor) {
this.delegate = delegate;
this.environmentEncryptor = environmentEncryptor;
}
@Override
public Environment findOne(String name, String profiles, String label) {
Environment environment = this.delegate.findOne(name, profiles, label);
if (this.environmentEncryptor != null) {
environment = this.environmentEncryptor.decrypt(environment);
}
if (!this.overrides.isEmpty()) {
environment.addFirst(new PropertySource("overrides", this.overrides));
}
return environment;
}
}
它有一个解密器environmentEncryptor
用于对加密存放的配置进行解密,另外包含一个EnvironmentRepository
的实现类delegate
,这里注入的类是SearchPathCompositeEnvironmentRepository
SearchPathCompositeEnvironmentRepository
本身并没有findOne()方法,由它的父类CompositeEnvironmentRepository
实现。
public class SearchPathCompositeEnvironmentRepository extends CompositeEnvironmentRepository implements SearchPathLocator {
public SearchPathCompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) {
super(environmentRepositories);
}
}
CompositeEnvironmentRepository有一个EnvironmentRepository的列表。从它的findOne()方法可以看出:当有多个配置存放方式时,CompositeEnvironmentRepository会遍历所有EnvironmentRepository来获取所有配置
public class CompositeEnvironmentRepository implements EnvironmentRepository {
protected List<EnvironmentRepository> environmentRepositories;
public CompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) {
//Sort the environment repositories by the priority
Collections.sort(environmentRepositories, OrderComparator.INSTANCE);
this.environmentRepositories = environmentRepositories;
}
@Override
public Environment findOne(String application, String profile, String label) {
Environment env = new Environment(application, new String[]{profile}, label, null, null);
if(environmentRepositories.size() == 1) {
Environment envRepo = environmentRepositories.get(0).findOne(application, profile, label);
env.addAll(envRepo.getPropertySources());
env.setVersion(envRepo.getVersion());
env.setState(envRepo.getState());
} else {
for (EnvironmentRepository repo : environmentRepositories) {
env.addAll(repo.findOne(application, profile, label).getPropertySources());
}
}
return env;
}
}
小结一下:虽然实现了EnvironmentRepository接口。但EnvironmentEncryptorEnvironmentRepository只是一个代理, SearchPathCompositeEnvironmentRepository/CompositeEnvironmentRepository也没有具体加载配置的逻辑。
而真正加载配置的类存放在CompositeEnvironmentRepository的environmentRepositories列表。
包括:
NativeEnvironmentRepository: 获取本地配置;
SvnRepositoryConfiguration: 获取存放在svn中的配置;
VaultEnvironmentRepository: 获取存放在vault中的配置;
GitRepositoryConfiguration:获取存放在git中
NativeEnvironmentRepository 用于加载本地(native)配置。它加载配置时,其实是以特定环境(传入的profile)启动了另外一个微型spring boot应用,通过这个应用获取所有的配置,然后调用clean过滤,得到所需配置。
@ConfigurationProperties("spring.cloud.config.server.native")
public class NativeEnvironmentRepository implements EnvironmentRepository, SearchPathLocator, Ordered {
@Override
public Environment findOne(String config, String profile, String label) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(
PropertyPlaceholderAutoConfiguration.class);
ConfigurableEnvironment environment = getEnvironment(profile);
builder.environment(environment);
builder.web(false).bannerMode(Mode.OFF);
if (!logger.isDebugEnabled()) {
// Make the mini-application startup less verbose
builder.logStartupInfo(false);
}
String[] args = getArgs(config, profile, label);
// Explicitly set the listeners (to exclude logging listener which would change
// log levels in the caller)
builder.application()
.setListeners(Arrays.asList(new ConfigFileApplicationListener()));
ConfigurableApplicationContext context = builder.run(args);
environment.getPropertySources().remove("profiles");
try {
return clean(new PassthruEnvironmentRepository(environment).findOne(config,
profile, label));
}
finally {
context.close();
}
}
private ConfigurableEnvironment getEnvironment(String profile) {
ConfigurableEnvironment environment = new StandardEnvironment();
environment.getPropertySources()
.addFirst(new MapPropertySource("profiles",
Collections.<String, Object>singletonMap("spring.profiles.active",
profile)));
return environment;
}
protected Environment clean(Environment value) {
Environment result = new Environment(value.getName(), value.getProfiles(),
value.getLabel(), this.version, value.getState());
for (PropertySource source : value.getPropertySources()) {
String name = source.getName();
if (this.environment.getPropertySources().contains(name)) {
continue;
}
name = name.replace("applicationConfig: [", "");
name = name.replace("]", "");
if (this.searchLocations != null) {
boolean matches = false;
String normal = name;
if (normal.startsWith("file:")) {
normal = StringUtils
.cleanPath(new File(normal.substring("file:".length()))
.getAbsolutePath());
}
String profile = result.getProfiles() == null ? null
: StringUtils.arrayToCommaDelimitedString(result.getProfiles());
for (String pattern : getLocations(result.getName(), profile,
result.getLabel()).getLocations()) {
if (!pattern.contains(":")) {
pattern = "file:" + pattern;
}
if (pattern.startsWith("file:")) {
pattern = StringUtils
.cleanPath(new File(pattern.substring("file:".length()))
.getAbsolutePath())
+ "/";
}
if (logger.isTraceEnabled()) {
logger.trace("Testing pattern: " + pattern
+ " with property source: " + name);
}
if (normal.startsWith(pattern)
&& !normal.substring(pattern.length()).contains("/")) {
matches = true;
break;
}
}
if (!matches) {
// Don't include this one: it wasn't matched by our search locations
if (logger.isDebugEnabled()) {
logger.debug("Not adding property source: " + name);
}
continue;
}
}
logger.info("Adding property source: " + name);
result.add(new PropertySource(name, source.getSource()));
}
return result;
}
}
在上面的分析可以知道,所有的配置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;
}
};
}
}
前面说到,调用EnvironmentController
接口返回的是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程序,可以查看到以下调用链路。
所以,可以知道在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);
}
}
@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