• Spring源码学习笔记(三、路径和占位符,Spring容器如何解析配置信息)


    目录:

    • 配置文件路径解析
    • 环境和属性
    • 源码分析

    配置文件路径解析

    在了解Spring容器如何解析配置文件路径前,我们先来看一段代码

    1 <?xml version="1.0" encoding="UTF-8"?>
    2 <beans xmlns="http://www.springframework.org/schema/beans"
    3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    5 
    6     <bean class="com.jdr.spring.TestBean" name="testBean"/>
    7 
    8 </beans>
    1 public class TestBean {
    2     public void run() {
    3         System.out.println("testBean run...");
    4     }
    5 }
    1 public class TestSpring {
    2     public static void main(String[] args) {
    3         ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
    4         TestBean testBean = (TestBean) ctx.getBean("testBean");
    5         testBean.run();
    6     }
    7 }

    上面这段代码通过ClassPathXmlApplicationContext来解析beans.xml,并且执行了TestBeanrun方法,我相信有点Spring基础的同学都是能看懂的。

    而本次学习的重点也不是如何使用ClassPathXmlApplicationContext,而是ClassPathXmlApplicationContext是如何解析beans.xml的。

    废话不多说,我们先看看TestSpring的源码。

    ———————————————————————————————————————————————————————

    通过查看源码我们可以发现,ClassPathXmlApplicationContext主要代码如下。

    1 public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
    2         throws BeansException {
    3 
    4     super(parent);
    5     setConfigLocations(configLocations);
    6     if (refresh) {
    7         refresh();
    8     }
    9 }

    首先第5行先设置了Spring上下文的配置位置,然后调用了refresh函数,此函数执行了Spring容器启动前的一些操作,不是本次的重点,后续再说。

    setConfigLocations的核心代码如下:

     1 public void setConfigLocations(String... locations) {
     2     if (locations != null) {
     3         Assert.noNullElements(locations, "Config locations must not be null");
     4         this.configLocations = new String[locations.length];
     5         for (int i = 0; i < locations.length; i++) {
     6             this.configLocations[i] = resolvePath(locations[i]).trim();
     7         }
     8     }
     9     else {
    10         this.configLocations = null;
    11     }
    12 }
    13 
    14 protected String resolvePath(String path) {
    15     return getEnvironment().resolveRequiredPlaceholders(path);
    16 }

    其中,setConfigLocations函数的代码很简单,就必不再赘述。我们直接看下resolvePath做了一些什么事。

    我们可以看到在调用了getEnvironment后,便又调用了resolveRequiredPlaceholders。这个地方就是占位符解析和替换的工作。接下来我们首先要了解的知识就是“Spring环境、属性和占位符”。

    环境和属性

    Spring环境和属性由四个部分组成:

    • PropertySource:属性源。key-value 属性对抽象,用于配置数据。
    • PropertyResolver:属性解析器。用于解析属性配置。
    • Profile:剖面。只有被激活的Profile才会将其中所对应的Bean注册到Spring容器中
    • Environment:环境。Profile和PropertyResolver的组合。

    详解:

    • PropertySource:提供了可配置属性源上的搜索操作
    • PropertyResolver:属性解析器,用于解析任何基础源的属性的接口。
    • ConfigurablePropertyResolver:提供属性类型转换的功能。
    • AbstractPropertyResolver:解析属性文件的抽象基类。设置了解析属性文件所需要ConversionServiceprefixsuffixvalueSeparator等信息。
    • PropertySourcesPropertyResolver:PropertyResolver的实现,对一组PropertySources提供属性解析服务
    • ConversionService:用于在运行时执行类型转换
    • Environment:集成在容器中的抽象,它主要包含两个方面,Profiles和Properties。
    • ConfigurableEnvironment:设置激活的profile默认的profile的功能以及操作Properties的工具

    ———————————————————————————————————————————————————————

    PropertySource:

    PropertySource是在Spring Environment之上提供了可配置属性源上的搜索操作,其核心代码如下。

     1 public abstract class PropertySource<T> {
     2 
     3     // 属性值名
     4     protected final String name;
     5     
     6     // 属性对象
     7     protected final T source;
     8 
     9     // 获取属性名
    10     public String getName() {
    11         return this.name;
    12     }
    13 
    14     // 获取属性对象
    15     public T getSource() {
    16         return this.source;
    17     }
    18 
    19     // 属性名是否存在
    20     public boolean containsProperty(String name) {
    21         return (getProperty(name) != null);
    22     }
    23 
    24     // 根据属性名获取属性
    25     public abstract Object getProperty(String name);
    26 }

    属性搜索过程是按照层次结构执行的。默认情况下,系统属性优先于环境变量

    因此,在调用env.getProperty("foo")时,如果在系统属性和环境变量中都设置了foo属性,则系统变量将优先于环境变量。

    完整层次结构如下所示,优先级最高的条目位于顶部

    • ServletConfig参数
    • ServletContext参数
    • JNDI环境变量(如:"java:comp/env/")
    • JVM system properties("-D"命令行参数,如-Dfoo="abcd")
    • JVM system environment(操作系统环境变量)

    注意:属性值不会被合并,而是会被前面的条目覆盖。

    ———————————————————————————————————————————————————————

    PropertyResolver:

    属性解析器,用于解析任何基础源的属性的接口,其接口定义如下:

     1 public interface PropertyResolver {
     2 
     3     // 是否包含指定key
     4     boolean containsProperty(String key);
     5 
     6     // 返回指定key对应的value,若没有则返回null
     7     String getProperty(String key);
     8 
     9     // 返回指定key对应的value,若没有则返回defaultValue
    10     String getProperty(String key, String defaultValue);
    11 
    12     // 返回指定key对应的value,并解析成指定类型。如果没有对应值则返回null
    13     <T> T getProperty(String key, Class<T> targetType);
    14 
    15     // 返回指定key对应的value,并解析成指定类型。如果没有对应值则返回defaultValue
    16     <T> T getProperty(String key, Class<T> targetType, T defaultValue);
    17 
    18     // 转换指定key的value为指定类型。如果没有则返回null
    19     // 如果value不能转换成指定类型,则抛出ConversionException
    20     @Deprecated
    21     <T> Class<T> getPropertyAsClass(String key, Class<T> targetType);
    22 
    23     // 返回指定key的value值,如果没有则抛出异常
    24     String getRequiredProperty(String key) throws IllegalStateException;
    25 
    26     // 转换指定key的value为指定类型,如果没有则抛出异常
    27     <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
    28 
    29     // 解析${}占位符并替换为getProperty方法返回的结果,无法解析的占位符会被忽略
    30     String resolvePlaceholders(String text);
    31 
    32     // 解析${}占位符并替换为getProperty方法返回的结果,无法解析的占位符会抛异常
    33     String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
    34 }

    源码分析

    最后我们来看下Spring容器到底是如何解析配置的。

     1 // 1、根据对应环境,解析path里的占位符
     2 protected String resolvePath(String path) {
     3     return getEnvironment().resolveRequiredPlaceholders(path);
     4 }
     5 
     6 // 2、对占位符进行解析
     7 @Override
     8 public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
     9     return this.propertyResolver.resolveRequiredPlaceholders(text);
    10 }
    11 
    12 // 3、创建占位符解析工具
    13 @Override
    14 public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    15     if (this.strictHelper == null) {
    16         this.strictHelper = createPlaceholderHelper(false);
    17     }
    18     return doResolvePlaceholders(text, this.strictHelper);
    19 }
    20 
    21 // 4、替换占位符
    22 public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    23     Assert.notNull(value, "'value' must not be null");
    24     return parseStringValue(value, placeholderResolver, new HashSet<String>());
    25 }

    可以从上述代码中得知,替换占位符的核心代码便是parseStringValue函数,我们来看看它的实现。

     1 protected String parseStringValue(
     2         String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
     3     // ex: value = spring-${config}.xml
     4     StringBuilder result = new StringBuilder(value);
     5 
     6     // 找到占位符前缀下标(${)
     7     int startIndex = value.indexOf(this.placeholderPrefix);
     8     while (startIndex != -1) {
     9         // 找到占位符前缀对应的后缀下标
    10         int endIndex = findPlaceholderEndIndex(result, startIndex);
    11         if (endIndex != -1) {
    12             // 获取result中startIndex + this.placeholderPrefix.length()到endIndex的值
    13             // 也就是占位符中的字符,spring-${config}.xml >>> config
    14             String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
    15             String originalPlaceholder = placeholder;
    16             if (!visitedPlaceholders.add(originalPlaceholder)) {
    17                 throw new IllegalArgumentException(
    18                         "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
    19             }
    20             // 递归调用,解析占位符中包含的占位符(嵌套占位符)
    21             placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
    22             // 获取占位符对应的值
    23             String propVal = placeholderResolver.resolvePlaceholder(placeholder);
    24             if (propVal == null && this.valueSeparator != null) {
    25                 int separatorIndex = placeholder.indexOf(this.valueSeparator);
    26                 if (separatorIndex != -1) {
    27                     String actualPlaceholder = placeholder.substring(0, separatorIndex);
    28                     String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
    29                     propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
    30                     if (propVal == null) {
    31                         propVal = defaultValue;
    32                     }
    33                 }
    34             }
    35             if (propVal != null) {
    36                 // 递归调用,解析先前解析的占位符值中包含的占位符
    37                 // 也就是解析完propVal后,propVal可能是占位符(占位符嵌套)
    38                 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
    39                 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
    40                 if (logger.isTraceEnabled()) {
    41                     logger.trace("Resolved placeholder '" + placeholder + "'");
    42                 }
    43                 startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
    44             }
    45             else if (this.ignoreUnresolvablePlaceholders) {
    46                 // Proceed with unprocessed value.
    47                 startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
    48             }
    49             else {
    50                 throw new IllegalArgumentException("Could not resolve placeholder '" +
    51                         placeholder + "'" + " in value "" + value + """);
    52             }
    53             visitedPlaceholders.remove(originalPlaceholder);
    54         }
    55         else {
    56             startIndex = -1;
    57         }
    58     }
    59 
    60     return result.toString();
    61 }
    62 
    63 private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
    64     // 因为后缀的下标肯定在前缀之后,所以要加上前缀的长度,才能精准定位到对应的后缀下标
    65     int index = startIndex + this.placeholderPrefix.length();
    66     int withinNestedPlaceholder = 0;
    67     while (index < buf.length()) {
    68         // StringUtils.substringMatch: buf的第index下标的字符是否为this.placeholderSuffix
    69         if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
    70             if (withinNestedPlaceholder > 0) {
    71                 withinNestedPlaceholder--;
    72                 index = index + this.placeholderSuffix.length();
    73             }
    74             else {
    75                 return index;
    76             }
    77         }
    78         else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
    79             withinNestedPlaceholder++;
    80             index = index + this.simplePrefix.length();
    81         }
    82         else {
    83             index++;
    84         }
    85     }
    86     return -1;
    87 }

    parseStringValue的核心逻辑在while循环中;从其循环逻辑来看,如果value入参不包含this.placeholderPrefix的话则直接返回value。

    this.placeholderPrefix则是resolveRequiredPlaceholders函数中createPlaceholderHelper传入的,我们来看看其逻辑。

     1 public static final String PLACEHOLDER_PREFIX = "${";
     2 public static final String PLACEHOLDER_SUFFIX = "}";
     3 public static final String VALUE_SEPARATOR = ":";
     4 
     5 private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
     6 private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
     7 private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
     8 
     9 private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    10     return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
    11             this.valueSeparator, ignoreUnresolvablePlaceholders);
    12 }

    从上述代码中,我们便可以知道parseStringValue中的几个属性值分别如下:

    • private final String placeholderPrefix = "${";
    • private final String placeholderSuffix = "}";
    • private final String valueSeparator = ":";
    • private final boolean ignoreUnresolvablePlaceholders = false;

    ———————————————————————————————————————————————————————

    有了上面这些基础的东西后,就能轻松阅读parseStringValue的逻辑了。

    所以我们上面的测试类TestSpring中的ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");解析出来的便还是原值beans.xml

    如果我们改成下面这样,也是也是能够解析出来的。

     1 public class TestSpring {
     2 
     3     public static void main(String[] args) {
     4         Properties properties = System.getProperties();
     5         properties.setProperty("config", "beans");
     6         ClassPathXmlApplicationContext xml = new ClassPathXmlApplicationContext("${config}.xml");
     7         TestBean testBean = (TestBean) xml.getBean("testBean");
     8         testBean.run();
     9     }
    10 }

    当然,如果你仔细分析了parseStringValue后,你会发现其实它是支持嵌套占位符的,比如这样:

     1 public class TestSpring {
     2 
     3     public static void main(String[] args) {
     4         Properties properties = System.getProperties();
     5         properties.setProperty("config", "beans");
     6         properties.setProperty("prefix", "spring");
     7         properties.setProperty("prefix-config", "${prefix}");
     8 
     9         ClassPathXmlApplicationContext xml = new ClassPathXmlApplicationContext("${prefix-config}-${config}.xml");
    10         TestBean testBean = (TestBean) xml.getBean("testBean");
    11         testBean.run();
    12     }
    13 }
  • 相关阅读:
    [服务器]Windows Server 2008 64位1核1G安装SQL Server2008
    [工作]离职了!好好休息一下
    [工作]IT连和IT恋产品已完成第一版,准备上线运营
    [SQL Server]储存过程中使用临时表循环操作数据
    [Swift]Xcode格式化代码快捷键
    [Swift]使用Alamofire传递参数时报错
    [工作]记录一下目前的工作
    [Swift]Swift图片显示方式设置,控件UIImageView的contentMode属性设置
    [Swift]创建桥接文件,Swift使用MJRefresh刷新插件
    我遇到了改变的机会
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/12913207.html
Copyright © 2020-2023  润新知