前面的文章讲到了Spring Cloud的配置中心,可以将配置集中存储在指定地方。这里有个问题,应用程序如何知道该去git、zookeeper或者其他地方读取配置文件呢?
对比Spring Cloud项目和Spring Boot项目,会发现Spring Cloud项目会多出一个名为bootstrap.properties的配置文件。项目启动时,会先加载bootstrap.properties中的配置项,根据该文件中的配置,加载对应的配置文件处理类,继而加载对应的配置。
本文以从zookeeper中读取配置为例,从源码角度展示配置加载过程。
SpringApplication.run()-> SpringApplication.prepareContext()-> SpringApplication.applyInitalizers()- > PropertySourceBootstrapConfiguration.initalize() 复制代码
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); } } 复制代码
整段代码中核心的两个类为 PropertySourceLocator 和 PropertySource 接下来看一下这两个类做了什么事情
public interface PropertySourceLocator { /** * @param environment The current Environment. * @return A PropertySource, or null if there is none. * @throws IllegalStateException if there is a fail-fast condition. */ PropertySource<?> locate(Environment environment); } 复制代码
如代码所示,该类其实是一个接口,只有一个方法,从environment中加载PropertySource
从类描述可以看出,该类就维护了我们需要的name/value这种键值对的属性。
该类中的一些方法,有没有很熟悉,getProperty.
代码看到这里,我们其实可以得出这样的结论, 其实就是需要实现一个PropertySourceLocator, 我们将可以从我们想要的地方获取到配置信息。
接下来我们看一下,如何从zookeeper中读取配置信息,其实现逻辑是否与我们在前面得出的结论一致。
我们看一下官方组件 spring-cloud-zookeeper-config 的源代码
图中圈出来的两个类正好和我们第二步中分析到的两个类看起来名字相关联,接下来,我们看一下其实现
ZookeeperPropertySource继承自AbstractZookeeperPropertySource,有点小失望,并不是继承自前面PropertySource, 不要灰心,我们再来看一下AbstractZookeeperPropertySource的实现
大失所望,除了名字和PropertySource有点关系外,仍然看不出其他关系。不过发现这次继承的该类是spring-core中的类,接下来需要查看spring源代码,找到该类,终于发现该类继承自PropertySource
接下来再看ZookeeperPropertySourceLocator 这个就简单多了,直接实现了第二步中提到的接口. 抱着一颗求知欲的心,看一下他是如何读取到zookeeper中的值的。
@Override public PropertySource<?> locate(Environment environment) { if (environment instanceof ConfigurableEnvironment) { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; String appName = env.getProperty("spring.application.name"); if (appName == null) { // use default "application" (which config client does) appName = "application"; log.warn( "spring.application.name is not set. Using default of 'application'"); } List<String> profiles = Arrays.asList(env.getActiveProfiles()); String root = this.properties.getRoot(); this.contexts = new ArrayList<>(); String defaultContext = root + "/" + this.properties.getDefaultContext(); this.contexts.add(defaultContext); addProfiles(this.contexts, defaultContext, profiles); StringBuilder baseContext = new StringBuilder(root); if (!appName.startsWith("/")) { baseContext.append("/"); } baseContext.append(appName); this.contexts.add(baseContext.toString()); addProfiles(this.contexts, baseContext.toString(), profiles); CompositePropertySource composite = new CompositePropertySource("zookeeper"); Collections.reverse(this.contexts); for (String propertySourceContext : this.contexts) { try { PropertySource propertySource = create(propertySourceContext); composite.addPropertySource(propertySource); // TODO: howto call close when /refresh } catch (Exception e) { if (this.properties.isFailFast()) { ReflectionUtils.rethrowRuntimeException(e); } else { log.warn("Unable to load zookeeper config from " + propertySourceContext, e); } } } return composite; } return null; } private PropertySource<CuratorFramework> create(String context) { return new ZookeeperPropertySource(context, this.curator); } 复制代码
看完该方法,我发现并没有直接读取zookeeper中的值,只是知道了他是使用的CuratorFramework这个框架,如果让我来试下,肯定有getNode或者是getData之类的方法读取zookeeper节点。仔细分析,发现其实在构造ZookeeperPropertySource时,将curator作为参数传递过去了。请看代码( 代码中的中文是我加的注释 )
public ZookeeperPropertySource(String context, CuratorFramework source) { super(context, source); findProperties(this.getContext(), null); } private void findProperties(String path, List<String> children) { try { log.trace("entering findProperties for path: " + path); if (children == null) { children = getChildren(path); } if (children == null || children.isEmpty()) { return; } //以下是核心代码 //递归遍历zookeeper的所有节点,获取节点的值并注册,如果该节点值为null,则转换为空字符串,这个需要注意。 for (String child : children) { String childPath = path + "/" + child; List<String> childPathChildren = getChildren(childPath); byte[] bytes = getPropertyBytes(childPath); if (bytes == null || bytes.length == 0) { if (childPathChildren == null || childPathChildren.isEmpty()) { registerKeyValue(childPath, ""); } } else { registerKeyValue(childPath, new String(bytes, Charset.forName("UTF-8"))); } // Check children even if we have found a value for the current znode findProperties(childPath, childPathChildren); } log.trace("leaving findProperties for path: " + path); } catch (Exception exception) { ReflectionUtils.rethrowRuntimeException(exception); } } //获取节点的值 private byte[] getPropertyBytes(String fullPath) { try { byte[] bytes = null; try { //我所提到的如果让我来实现,可能会调用的getData bytes = this.getSource().getData().forPath(fullPath); } catch (KeeperException e) { if (e.code() != KeeperException.Code.NONODE) { // not found throw e; } } return bytes; } catch (Exception exception) { ReflectionUtils.rethrowRuntimeException(exception); } return null; } //注册值,这个比较简单了。 private void registerKeyValue(String path, String value) { String key = sanitizeKey(path); this.properties.put(key, value); } 复制代码
如果某个配置项存在于多个配置位置(环境变量、系统属性、命令行参数、内部配置文件,外部配置文件),那么会取哪一个配置中的值作为最终值呢。这就涉及到Spring Boot配置的加载顺序了,等后续有时间在写一篇文章向大家介绍。