本文主要从一些spring-cloud-config-server
包中的注解和类来分析配置中心是如何对外提供配置。
从@EnableConfigServer开始
为了让一个spring boot
应用成为配置中心,我们需要使用@EnableConfigServer
注解
@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
装配了一个Marker
Bean。这个bean则有开启了ConfigServerAutoConfiguration(看下面的代码中的@ConditionalOnBean参数)
@Configuration @ConditionalOnBean(ConfigServerConfiguration.Marker.class) @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class, TransportConfiguration.class }) public class ConfigServerAutoConfiguration { }
这里又引入了多个配置类,包括:
EnvironmentRepositoryConfiguration
CompositeConfiguration
ResourceRepositoryConfiguration
ConfigServerEncryptionConfiguration
ConfigServerMvcConfiguration
TransportConfiguration
接下来介绍EnvironmentRepositoryConfiguration
EnvironmentRepositoryConfiguration
EnvironmentRepositoryConfiguration
是配置中心的关键Configuration
类。这个配置类中包含很多实现了EnvironmentRepository
接口的类,每个实现类都对应一种类型(git/svn/navtie/vault)的配置。 EnvironmentRepositoryConfiguration
通过profile
注解(对当前应用的环境)决定使用装配哪个EnvironmentRepository
Bean。默认是MultipleJGitEnvironmentRepository
@Configuration @Import({ JdbcRepositoryConfiguration.class, VaultRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class, DefaultRepositoryConfiguration.class }) public class EnvironmentRepositoryConfiguration { @Bean @ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true) public ConfigServerHealthIndicator configServerHealthIndicator( EnvironmentRepository repository) { return new ConfigServerHealthIndicator(repository); } @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(); } } } @Configuration @ConditionalOnMissingBean(EnvironmentRepository.class) 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 @ConditionalOnMissingBean(EnvironmentRepository.class) @Profile("native") class NativeRepositoryConfiguration { @Autowired private ConfigurableEnvironment environment; @Autowired private ConfigServerProperties configServerProperties; @Bean public NativeEnvironmentRepository nativeEnvironmentRepository() { NativeEnvironmentRepository repository = new NativeEnvironmentRepository( this.environment); repository.setDefaultLabel(configServerProperties.getDefaultLabel()); return repository; } } @Configuration @Profile("git") class GitRepositoryConfiguration extends DefaultRepositoryConfiguration { } @Configuration @Profile("subversion") 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") class VaultRepositoryConfiguration { @Bean public VaultEnvironmentRepository vaultEnvironmentRepository( HttpServletRequest request, EnvironmentWatch watch) { return new VaultEnvironmentRepository(request, watch, new RestTemplate()); } } @Configuration @Profile("jdbc") class JdbcRepositoryConfiguration { @Bean public JdbcEnvironmentRepository jdbcEnvironmentRepository(JdbcTemplate jdbc) { return new JdbcEnvironmentRepository(jdbc); } }
EnvironmentRepository
EnvironmentRepository
是一个配置管理仓库接口,抽象了获取配置的方法:
Environment findOne(String application, String profile, String label);
它的实现类有很多,如下图所示:
从名字中大概可以看出,这些类应该是用于加载不同类型的配置(后面会再介绍)。
上面说的主要是configserver服务端如何自动配置和加载配置文件的,下面说说这些配置如何暴露在微服务系统中。对外提供接口的类,就是EnvironmentController
交互入口1: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}/{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("(_)", "/"); } StopWatch sw = new StopWatch("labelled"); sw.start(); logger.info("EnvironmentController.labelled()开始,name={},profiles={},label={}", name, profiles, label); Environment environment = this.repository.findOne(name, profiles, label); sw.stop(); logger.info("EnvironmentController.labelled()结束,name={},profiles={},label={},耗时={}", name, profiles, label, sw.getTotalTimeMillis()); return environment; }
在labelled
方法中,会调用repository
的findOne()来加载配置,然后返回给配置获取方。
各式各样的配置仓库类
EnvironmentEncryptorEnvironmentRepository
前面提到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
SearchPathCompositeEnvironmentRepository
本身并没有findOne()方法,由它的父类CompositeEnvironmentRepository
实现。
public class SearchPathCompositeEnvironmentRepository extends CompositeEnvironmentRepository implements SearchPathLocator { public SearchPathCompositeEnvironmentRepository(List<EnvironmentRepository> environmentRepositories) { super(environmentRepositories); } }
CompositeEnvironmentRepository
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
。
NativeEnvironmentRepository
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; } }
交互入口2:ConfigServerHealthIndicator,健康检查
ConfigServerHealthIndicator怎么加入到HealthEndpoint的,及怎么被调用的:
上面的调用是遍历indicators,每个indicator代表一种健康检查的HealthIndicator的实现类。
这些indicator是怎么加入的呢?
根据HealthIndicator接口类调用BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this, requiredType, true, descriptor.isEager());找到所有实现类。然后把它放到上面的indicators里。
在EndpointAutoConfiguration的构造函数中,发起上述的调用。
在具体看ConfigServerHealthIndicator
config-server和config-server client都有这个类,用于对资源配置中心的EnvironmentRepository是否正常工作的检测。
config-server服务端
config-server服务端的健康检查的自动配置的代码如下,同时可见spring.cloud.config.server.health.enabled的配置项是一个开关。
@Configuration @Import({ JdbcRepositoryConfiguration.class, VaultRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class, DefaultRepositoryConfiguration.class }) public class EnvironmentRepositoryConfiguration { @Bean @ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true) public ConfigServerHealthIndicator configServerHealthIndicator( EnvironmentRepository repository) { return new ConfigServerHealthIndicator(repository); }
config-server端的ConfigServerHealthIndicator的源码:
@ConfigurationProperties("spring.cloud.config.server.health") public class ConfigServerHealthIndicator extends AbstractHealthIndicator { private EnvironmentRepository environmentRepository; private Map<String, Repository> repositories = new LinkedHashMap<>(); public ConfigServerHealthIndicator(EnvironmentRepository environmentRepository) { this.environmentRepository = environmentRepository; } @PostConstruct public void init() { if (this.repositories.isEmpty()) { this.repositories.put("app", new Repository()); } } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { builder.up(); List<Map<String, Object>> details = new ArrayList<>(); for (String name : this.repositories.keySet()) { Repository repository = this.repositories.get(name); String application = (repository.getName() == null)? name : repository.getName(); String profiles = repository.getProfiles(); try { Environment environment = this.environmentRepository.findOne(application, profiles, repository.getLabel()); HashMap<String, Object> detail = new HashMap<>(); detail.put("name", environment.getName()); detail.put("label", environment.getLabel()); if (environment.getProfiles() != null && environment.getProfiles().length > 0) { detail.put("profiles", Arrays.asList(environment.getProfiles())); } if (!CollectionUtils.isEmpty(environment.getPropertySources())) { List<String> sources = new ArrayList<>(); for (PropertySource source : environment.getPropertySources()) { sources.add(source.getName()); } detail.put("sources", sources); } details.add(detail); } catch (Exception e) { HashMap<String, String> map = new HashMap<>(); map.put("application", application); map.put("profiles", profiles); builder.withDetail("repository", map); builder.down(e); return; } } builder.withDetail("repositories", details); }
config-server client端
config-server client端的健康检查的自动配置的代码如下,同时可见health.config.enabled是是否对config-server的健康检查的开关(config-server端的资源配置中心的EnvironmentRepository是否正常工作的检测)
@Configuration public class ConfigClientAutoConfiguration { @Configuration @ConditionalOnClass(HealthIndicator.class) @ConditionalOnBean(ConfigServicePropertySourceLocator.class) @ConditionalOnProperty(value = "health.config.enabled", matchIfMissing = true) protected static class ConfigServerHealthIndicatorConfiguration { @Bean public ConfigServerHealthIndicator configServerHealthIndicator( ConfigServicePropertySourceLocator locator, ConfigClientHealthProperties properties, Environment environment) { return new ConfigServerHealthIndicator(locator, environment, properties); } }
健康检查的核心代码:
public class ConfigServerHealthIndicator extends AbstractHealthIndicator { public ConfigServerHealthIndicator(ConfigServicePropertySourceLocator locator, Environment environment, ConfigClientHealthProperties properties) { this.environment = environment; this.locator = locator; this.properties = properties; } @Override protected void doHealthCheck(Builder builder) throws Exception { PropertySource<?> propertySource = getPropertySource(); builder.up(); if (propertySource instanceof CompositePropertySource) { List<String> sources = new ArrayList<>(); for (PropertySource<?> ps : ((CompositePropertySource) propertySource).getPropertySources()) { sources.add(ps.getName()); } builder.withDetail("propertySources", sources); } else if (propertySource!=null) { builder.withDetail("propertySources", propertySource.toString()); } else { builder.unknown().withDetail("error", "no property sources located"); } }