写点什么

spring4.1.8 初始化源码学习三部曲之二:setConfigLocations 方法

作者:程序员欣宸
  • 2022 年 6 月 08 日
  • 本文字数:3931 字

    阅读完需:约 13 分钟

spring4.1.8初始化源码学习三部曲之二:setConfigLocations方法

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos


整体概括

  • 本章会涉及到多个类的细节,所以先从整体上概括 AbstractRefreshableConfigApplicationContext.setConfigLocations 方法的主要功能,后面的细节都是基于这些总结展开的:


  1. setConfigLocations 主要工作有两个:创建环境对象 ConfigurableEnvironment 、处理 ClassPathXmlApplicationContext 传入的字符串中的占位符;

  2. 环境对象 ConfigurableEnvironment 中包含了当前 JVM 的 profile 配置信息、环境变量、 Java 进程变量;

  3. 处理占位符的关键是 ConfigurableEnvironment、PropertyResolver、PropertyPlaceholderHelper 之间的配合:



  • 用思维导图来辅助:


展开详情

  • 接下来去阅读 setConfigLocations 方法内部的细节代码:

  • 跟踪该方法,找到是在类 AbstractRefreshableConfigApplicationContext 中实现的:


public void setConfigLocations(String... locations) {  if (locations != null) {    Assert.noNullElements(locations, "Config locations must not be null");    this.configLocations = new String[locations.length];    for (int i = 0; i < locations.length; i++) {      this.configLocations[i] = resolvePath(locations[i]).trim();    }  }  else {    this.configLocations = null;  }}
复制代码


  • 从上述代码可以发现,本章我们要重点学习的是 resolvePath(locations[i]),结合上一章 demo 中的入参,此处应该是方法 resolvePath("classpath:applicationContext.xml")

  • 跟踪到 AbstractRefreshableConfigApplicationContext 类,这个方法的目的是替换掉 path 字符串中的占位符 ${XXX}这样的内容:


protected String resolvePath(String path) {  return getEnironment().resolveRequiredPlaceholders(path);}
复制代码


  • 先看 getEnvironment()方法:


@Overridepublic ConfigurableEnvironment getEnvironment() {  if (this.environment == null) {    this.environment = createEnvironment();  }  return this.environment;}
复制代码


  • 关于 ConfigurableEnvironment 接口,我们先来看看他的声明方法以及继承关系:



从上图可见,ConfigurableEnvironment 接口有两个重要部分组成:Profile 和 Property;Profile 是对测试、生产等不同环境下的 bean 配置,这里我们没有特别设置,所以用到的 profile 是 AbstractEnvironment 的 defaultProfiles;接下来关于 Property 资源是如何产生的;


  • 顺着调用一路看过去,发现最终会调用 AbstractEnvironment 类的构造方法:


public AbstractEnvironment() {  customizePropertySources(this.propertySources);  if (this.logger.isDebugEnabled()) {    this.logger.debug(format(        "Initialized %s with PropertySources %s", getClass().getSimpleName(), this.propertySources));  }}
复制代码


  • 上面的 customizePropertySources 是在 StandardEnvironment 类中实现的,如下:


@Overrideprotected void customizePropertySources(MutablePropertySources propertySources) {  propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));  propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));}
复制代码


  • 上述代码中,可以将 propertySources 对象理解成一个容器(其对象内部核心成员 propertySourceList 是个 CopyOnWriteArrayList 实例);

  • 首先向 propertySources 添加一组属性,来自 Java 进程变量(getSystemProperties()内是 System.getProperties()方法);

  • 接着向 propertySources 再添加一组属性,来自系统环境变量(getSystemEnvironment()内是 System.getenv()方法);

  • getSystemProperties 和 getSystemEnvironment 方法中有个相同的细节需要注意,在获取进程变量或者系统环境变量的时候,都有可能因为安全限制抛出异常,这时候就返回一个 ReadOnlySystemAttributesMap 的实现类,外部调用 get 方法的时候,再去尝试获取进程变量或者系统环境变量对应的值,取不到则返回 null,代码如下:


public Map<String, Object> getSystemProperties() {  try {    return (Map) System.getProperties();  }  catch (AccessControlException ex) {    return (Map) new ReadOnlySystemAttributesMap() {      @Override      protected String getSystemAttribute(String attributeName) {        try {          return System.getProperty(attributeName);        }        catch (AccessControlException ex) {          if (logger.isInfoEnabled()) {            logger.info(format("Caught AccessControlException when accessing system " +                "property [%s]; its value will be returned [null]. Reason: %s",                attributeName, ex.getMessage()));          }          return null;        }      }    };  }}
复制代码


  • StandardEnvironment 对象创建成功后,接着看它的 resolveRequiredPlaceholders 方法:


@Overridepublic String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {  return this.propertyResolver.resolveRequiredPlaceholders(text);}
复制代码


  • 上面的 propertyResolver 从何而来?以下代码可见,这个 final 型的成员变量在声明时就创建了,前面准备好的 propertySources 集合通过构造方法传给了它,所有它已经获得了所有系统环境变量和进程环境变量:


private final ConfigurablePropertyResolver propertyResolver =      new PropertySourcesPropertyResolver(this.propertySources);
复制代码


  • 跟踪 PropertySourcesPropertyResolver.resolveRequiredPlaceholders,发现真正处理占位符的逻辑是在 PropertyPlaceholderHelper.doResolvePlaceholders 方法:


private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {    return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {      @Override      public String resolvePlaceholder(String placeholderName) {        return getPropertyAsRawString(placeholderName);      }    });  }
复制代码


  • getPropertyAsRawString 的具体实现在 PropertySourcesPropertyResolver 类中:


@Overrideprotected String getPropertyAsRawString(String key) {  return getProperty(key, String.class, false);}
复制代码


  • 继续跟踪 helper.replacePlaceholders(),到了 PropertyPlaceholderHelper.parseStringValue 方法,这里面逐一找出每个占位符去做替换:


public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {  Assert.notNull(value, "'value' must not be null");  return parseStringValue(value, placeholderResolver, new HashSet<String>());}
复制代码


  • parseStringValue 方法中,找到了占位符后,会调用入参 placeholderResolver 的 resolvePlaceholder(placeholder)方法,也就是步骤 9 中匿名类的 getPropertyAsRawString 方法(实际上就是 PropertySourcesPropertyResolver.getPropertyAsRawString 方法),最终会在 PropertySourcesPropertyResolver.getProperty 方法中找出所有的属性来匹配占位符:


protected String parseStringValue(      String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
printTrack("start parseStringValue"); logger.info("before parse : [" + strVal + "]");
StringBuilder result = new StringBuilder(strVal);
int startIndex = strVal.indexOf(this.placeholderPrefix); while (startIndex != -1) { int endIndex = findPlaceholderEndIndex(result, startIndex); if (endIndex != -1) { String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); String originalPlaceholder = placeholder; if (!visitedPlaceholders.add(originalPlaceholder)) { throw new IllegalArgumentException( "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); } // Recursive invocation, parsing placeholders contained in the placeholder key. //这里有迭代操作,确保处理完字符串中所有的占位符 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); // 这里实际上会调用PropertySourcesPropertyResolver.getPropertyAsRawString方法,propVal的值就是从环境变量中取得的值 String propVal = placeholderResolver.resolvePlaceholder(placeholder); ...
复制代码


  • 至此,Spring 环境的初始化准备工作已经完成,下一章一起去看 refresh()方法,那里聚集了 Spring 初始化的核心操作:《spring4.1.8 初始化源码学习三部曲之三:AbstractApplicationContext.refresh 方法》

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

发布于: 刚刚阅读数: 3
用户头像

搜索"程序员欣宸",一起畅游Java宇宙 2018.04.19 加入

前腾讯、前阿里员工,从事Java后台工作,对Docker和Kubernetes充满热爱,所有文章均为作者原创,个人Github:https://github.com/zq2599/blog_demos

评论

发布
暂无评论
spring4.1.8初始化源码学习三部曲之二:setConfigLocations方法_Java_程序员欣宸_InfoQ写作社区