1. 程式人生 > 其它 >Spring-外部配置的值是如何通過@Value註解獲取的?

Spring-外部配置的值是如何通過@Value註解獲取的?

技術標籤:Spring Corejavaspringiocaopbean

在DefaultListableBeanFactory的doResolveDependency方法中,首先通過AutowireCandidateResolver的getSuggestedValue方法獲取@Value註解的屬性值後(例如在@Value註解中配置的value屬性為 “ u s e r . n a m e ” , 那 這 裡 獲 取 到 的 就 是 " {user.name}”,那這裡獲取到的就是" user.name"{user.name}”)。

@Value
("${user.name}") ------> "${user.name}" // 經過#getSuggestedValue方法處理後 private String userName;

接下來判斷獲取到的值是否不等於null,如果不等於null,則判斷值是否是String型別的,如果是Stri-ng型別的,首先呼叫resolveEmbeddedValue方法。在@Value註解中配置的EL表示式就是在這裡完成解析的。

// DefaultListableBeanFactory#doResolveDependency
public Object doResolveDependency
(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); try { Object shortcut =
descriptor.resolveShortcut(this); if (shortcut != null) { return shortcut; } Class<?> type = descriptor.getDependencyType(); // 這裡使用的ContextAnnotationAautowireCandidateResolver Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); if (value != null) { if (value instanceof String) { // 解析EL表示式 String strVal = resolveEmbeddedValue((String) value); BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); value = evaluateBeanDefinitionString(strVal, bd); } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); try { return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor()); } catch (UnsupportedOperationException ex) { // A custom TypeConverter which does not support TypeDescriptor resolution... return (descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); } } // 只貼出和本次分析相關的程式碼,其餘程式碼省略... }

resolveEmbeddedValue方法定義在AbstractBeanFactory類中,在該方法中通過遍歷所有註冊的Strin-gValueResolver介面實現類的resolveStringValue方法來解析傳入的value,該迴圈正常情況下的結束條件是當前正遍歷的StringValueResolver的resolveStringValue方法返回值為null或者遍歷完畢,即使上一個StringValueResolver實現類已經解析完傳入的value,下一個(如果有)實現類依然可以解析。

// AbstractBeanFactory#resolveEmbeddedValue
public String resolveEmbeddedValue(@Nullable String value) {
   if (value == null) {
      return null;
   }
   String result = value;
   for (StringValueResolver resolver : this.embeddedValueResolvers) {
      result = resolver.resolveStringValue(result);
      if (result == null) {
         return null;
      }
   }
   return result;
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-iJhbyNcb-1608707279814)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ed7bac7-ce97-4a2c-8488-b1c5e44bbcf0/Untitled.png)]

應用上下文預設情況下往IoC容器中註冊了一個StringValueResolver實現類-在AbstractApplicationCo-ntext的finishBeanFactoryInitialization方法中,通過Lambda表示式註冊。

在該實現中通過呼叫Environment實現類的resolvePlaceholders方法來完成解析。

// AbstractApplicationContext#finishBeanFactoryInitialization
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // 只貼出和本次分析相關的程式碼,其餘程式碼省略...
   if (!beanFactory.hasEmbeddedValueResolver()) {
      beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
   }
   // 只貼出和本次分析相關的程式碼,其餘程式碼省略...
}

resolvePlaceholers方法由PropertyResolver介面定義,由AbstractEnvironment來實現。在該方法中,通過呼叫提前例項化好的ConfigurablePropertyResolver介面實現類PropertySourcePropertyResolver的resolvePlaceholders方法來完成。

在建立PropertySourcesPropertyResolver時傳入在AbstractEnvironment物件中定義的MutableProper-tySources物件例項作為構造引數。

private final MutablePropertySources propertySources = new MutablePropertySources();

private final ConfigurablePropertyResolver propertyResolver =
			new PropertySourcesPropertyResolver(this.propertySources);
// AbstractEnvironment#resolvePlaceholders
public String resolvePlaceholders(String text) {
   return this.propertyResolver.resolvePlaceholders(text);
}

PropertySourcePropertyResolver並未實現resolvePlaceholders方法,而是由其父類AbstractProperty-Resolver來實現。在該實現中,首先判斷nonStrictHelper屬性是否為空,如果為空則呼叫createPlace-HolderHelper方法建立PropertyPlaceholderHelper例項並賦值給nonStrictHelper。

PropertyPlaceholderHelper位於spring core中的工具包中,該類並未實現任何介面。需注意的是在建立PropertyPlaceholderHelper時,傳入的幾個構造引數,它們分別為:placeholderPrefix(佔位符字首"${")、placeholderSuffix(佔位符字尾"}"),valueSeparator(值分隔符“:”),ignoreUnresolvablePlace-holders(,忽略不能解析的佔位符,預設為true)。

private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;

private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;

/** Prefix for system property placeholders: "${". */
public static final String PLACEHOLDER_PREFIX = "${";

/** Suffix for system property placeholders: "}". */
public static final String PLACEHOLDER_SUFFIX = "}";

/** Value separator for system property placeholders: ":". */
public static final String VALUE_SEPARATOR = ":";

// AbstractPropertyResolver#resolvePlaceholders
public String resolvePlaceholders(String text) {
   if (this.nonStrictHelper == null) {
      this.nonStrictHelper = createPlaceholderHelper(true);
   }
   return doResolvePlaceholders(text, this.nonStrictHelper);
}

// AbstractPropertyResolver#createPlaceholderHelper
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
		return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
				this.valueSeparator, ignoreUnresolvablePlaceholders);
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-DqEmMgo7-1608707279821)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/70638a6c-961d-406d-900a-3204c9c66b81/Untitled.png)]

doResolvePlaceholders方法定義在AbstractPropertyResolver類中,這是一個私有方法。在該方法中呼叫傳入的helper的replacePlaceholders方法,並傳入了一個方法引用。

// AbstractPropertyResolver#doResolvePlaceholders
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
   return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

在PropertyPlaceholderHelper的replacePlaceholders方法中,對傳入的value進行斷言後,直接呼叫p-arseStringValue方法並返回。

// PropertyPlaceholderHelper#replacePlaceholders(String, PropertyPlaceholderHelper.PlaceholderResolver)
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
   Assert.notNull(value, "'value' must not be null");
   return parseStringValue(value, placeholderResolver, null);
}

先檢視該方法全部原始碼,有個總體印象,接下來會分段分析。

// PropertyPlaceholderHelper#parseStringValue
protected String parseStringValue(
      String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {

   int startIndex = value.indexOf(this.placeholderPrefix);
   if (startIndex == -1) {
      return value;
   }
   StringBuilder result = new StringBuilder(value);
   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 == null) {
            visitedPlaceholders = new HashSet<>(4);
         }
         if (!visitedPlaceholders.add(originalPlaceholder)) {
            throw new IllegalArgumentException(
                  "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
         }
         placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
         String propVal = placeholderResolver.resolvePlaceholder(placeholder);
         if (propVal == null && this.valueSeparator != null) {
            int separatorIndex = placeholder.indexOf(this.valueSeparator);
            if (separatorIndex != -1) {
               String actualPlaceholder = placeholder.substring(0, separatorIndex);
               String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
               propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
               if (propVal == null) {
                  propVal = defaultValue;
               }
            }
         }
         if (propVal != null) {
            propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
            result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
            if (logger.isTraceEnabled()) {
               logger.trace("Resolved placeholder '" + placeholder + "'");
            }
            startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
         } else if (this.ignoreUnresolvablePlaceholders) {
            startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
         } else {
            throw new IllegalArgumentException("Could not resolve placeholder '" +
                  placeholder + "'" + " in value \"" + value + "\"");
         }
         visitedPlaceholders.remove(originalPlaceholder);
      } else {
         startIndex = -1;
      }
   }
   return result.toString();
}

在parseStringValue方法中 ,首先通過String提供的indexOf方法來獲取佔位符字首("${")的下標,如果該下標等於-1,即未找到,則不解析,直接原值返回。

 int startIndex = value.indexOf(this.placeholderPrefix);
   if (startIndex == -1) {
      return value;
   }

接下來建立一個StringBuilder物件,初始值為傳遞的value。執行while迴圈,結束條件是佔位符字首的下標不等於-1。呼叫findPlaceholderEndIndex方法,傳入前面建立的StringBuilder物件以及佔位符字首的下標。如果該方法返回的佔位符字尾("}")的下標等於-1,則將startIndex賦值為-1,迴圈結束。

 StringBuilder result = new StringBuilder(value);
   while (startIndex != -1) {
      int endIndex = findPlaceholderEndIndex(result, startIndex);
      if (endIndex != -1) {
			// 省略其它程式碼...
			} else {
         startIndex = -1;
      }

接下來判斷獲取到的佔位符字尾("}")下標是否不等於-1,如果判斷成立,從StringBuilder物件(以下按變數名result)中從佔位符字首長度開始擷取直到前面獲取到的佔位符字尾下標處並賦值給String型別的變數-placeholder。判斷方法入參vistedPlacceholder是否等於null,如果等於null,則建立一個初始長度為4的HashSet,將前面處理好的字串新增到該集合中,如果新增失敗則丟擲異常。

String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
  visitedPlaceholders = new HashSet<>(4);
}
if (!visitedPlaceholders.add(originalPlaceholder)) {
  throw new IllegalArgumentException(
        "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}

遞迴呼叫當前方法(parseStringValue)來繼續處理本次處理完畢的字串。重點是接下來呼叫placeho-lderResolver的resolvePlaceholder方法,在該方法中完成了從Environment實現類物件中獲取對應屬性值,並將獲取到的屬性值賦值給propVal變數。

placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
String propVal = placeholderResolver.resolvePlaceholder(placeholder);

這裡呼叫的是在前面doResolvePlaceholders方法中傳遞的PropertySourcesPropertyResolver的getPr-opertyAsRawString方法引用。如果該方法返回值等於null並且值分隔符(":")不等於null,那麼則開始處理存在預設值的情況(例如@Value("${user.name:zhangsan}"))。

首先呼叫placeholder的indexOf方法來判斷是否存在值分隔符(“:”),如果獲取到的下標不等於-1,則呼叫placeholder的subString方法來從第零位擷取到獲取的值分隔符起始下標處,並賦值給actualPla-ceholder。然後繼續呼叫placeholder的subString方法,從值分隔符起始下標處加上值分隔符長度位擷取獲取預設值。

if (propVal == null && this.valueSeparator != null) {
    int separatorIndex = placeholder.indexOf(this.valueSeparator);
    if (separatorIndex != -1) {
       String actualPlaceholder = placeholder.substring(0, separatorIndex);
       String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
       propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
       if (propVal == null) {
          propVal = defaultValue;
       }
    }
 }

繼續呼叫PropertySourcesPropertyResolver的resolvePlaceholder方法來解析擷取掉預設值的佔位符,如果這次獲取到的值還是等於null,那麼則將預設值賦值給propVal變數。

接下來判斷前面通過PlaceholderResolver的resolvePlaceholder方法或者解析使用者在@Value註解中配置的預設值得到的propVal變數是否不等於null,如果不等於null,繼續遞迴呼叫當前方法(parseStrin-gValue)傳入propVal。呼叫result的replace方法,從佔位符字首("KaTeX parse error: Expected 'EOF', got '}' at position 79: …exOf方法,從佔位符字尾("}̲")起始下標+propVal長…{")下標並賦值給startIndex變數。通常獲取到的都是-1。

if (propVal != null) {
    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
    result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
    if (logger.isTraceEnabled()) {
       logger.trace("Resolved placeholder '" + placeholder + "'");
    }
    startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
 }

如果propVal等於空,則判斷ignoreUnresolvablePlaceholdes屬性是否為true,如果為true,則呼叫result的indexOf方法獲取佔位符字首的下標,從佔位符字尾("}")起始索引位+佔位符字尾長度位開始查詢,並將查詢到的結果賦值給startIndex變數。

else if (this.ignoreUnresolvablePlaceholders) {
  startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}

如果propVal等於空並且ignoreUnresolvablePlaceholders屬性為false,那麼則丟擲異常。

else {
  throw new IllegalArgumentException("Could not resolve placeholder '" +
        placeholder + "'" + " in value \"" + value + "\"");
}

最終呼叫result的toString方法並返回。

return result.toString();

接下來就分析下PropertyPlaceholderHelper的findPlaceholderEndIndex方法是如何從給定字串中找到佔位符字尾("}")的下標的。

首先根據傳入的佔位符字首("KaTeX parse error: Expected '}', got 'EOF' at end of input: …起始下標索引加上佔位符字首("{")長度得到從給定字串要開始查詢的起始下標(index),接下來就是while迴圈,該迴圈的終止條件是index小於給定的字串長度。

在這個while迴圈中,每一次遍歷都會呼叫StringUtils的subStringMatch方法,傳入給定的字串,以及當前下標和要匹配的子字串。這裡可以看到無論是if還是else都是呼叫StringUtils的subStringMa-tch方法,它們之間的呼叫區別就在於傳入的子字串不一樣。

在if中,呼叫StringUtils的subStringMatch方法時傳入的子字串為佔位符字尾("}"),而在else if中呼叫StringUtils的subStringMatch方法時傳入的子字串為簡單字首("{",注意不是佔位符字首“${”)。簡單佔位符字首通常用在巢狀佔位符中。

// PropertyPlaceholderHelper#findPlaceholderEndIndex
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
   int index = startIndex + this.placeholderPrefix.length();
   int withinNestedPlaceholder = 0;
   while (index < buf.length()) {
      if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
         if (withinNestedPlaceholder > 0) {
            withinNestedPlaceholder--;
            index = index + this.placeholderSuffix.length();
         } else {
            return index;
         }
      } else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
         withinNestedPlaceholder++;
         index = index + this.simplePrefix.length();
      } else {
         index++;
      }
   }
   return -1;
}

這裡就分析下StringUtils的subStringMatch方法是如何進行子串匹配的。

首先是判斷傳入的下標+子串長度是否大於字串的長度,如果判斷成立直接返回false。接下來開始迴圈,迴圈結束的條件是自增變數-i(i從0開始自增)小於子串的長度。

在該迴圈中,通過呼叫字串的charAt方法來獲取呼叫substringMatch方法時傳入的下標+1位置的字元,使用該字元和子串中第i位的字元進行等等比較,如果有任何一個字元不相等直接返回false。最終返回true。

// StringUtils#substringMatch
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
   if (index + substring.length() > str.length()) {
      return false;
   }
   for (int i = 0; i < substring.length(); i++) {
      if (str.charAt(index + i) != substring.charAt(i)) {
         return false;
      }
   }
   return true;
}

PlaceholderResolver的resolvePlaceholder方法是如何解析佔位符並返回對應的值呢?

前面已經分析過在呼叫PropertyPlaceholderHelper的replacePlaceholders方法是傳入的PlaceholderR-esolver為AbstractPropertyResolver的getPropertyAsRawString方法引用,因此在PropertyPlaceholde-rHelper的parseStringValue方法中呼叫PlaceholderResolver的resolvePlaceholder方法時,實際上呼叫的是PropertySourcesPropertyResolver的getPropertyAsRawString方法。

在getPropertyAsRawString方法中通過呼叫getProperty方法來完成,需注意的是這裡在呼叫getProp-erty方法是傳入的resolveNestedPlaceholders為false。

// PropertySourcesPropertyResolver#getPropertyAsRawString
protected String getPropertyAsRawString(String key) {
   return getProperty(key, String.class, false);
}

前面我們已經分析過,在建立PropertySourcesPropertyResolver物件例項時,需要傳入一個Propert-ySources物件,AbstractEnvironment傳入的是自己持有的PropertySources,因此在getProperty方法中遍歷的PropertySources實際是AbstractEnvironment的PropertySources。

遍歷PropertySources集合,獲取到每一個PropertySource物件,然後呼叫其getProperty方法,如果該方法的返回值不等於null,則判斷呼叫該方法時傳入的resolveNestedPlaceholders是否為true並且值是String型別的,則呼叫resolveNestedPlaceholders方法來解析巢狀的佔位符。通過前面的分析我們可以得知傳入的resolveNestedPlaceholders為false。

最後呼叫convertValueIfNecessary方法來進行型別轉換並返回。

// PropertySourcesPropertyResolver#getProperty
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
   if (this.propertySources != null) {
      for (PropertySource<?> propertySource : this.propertySources) {
         if (logger.isTraceEnabled()) {
            logger.trace("Searching for key '" + key + "' in PropertySource '" +
                  propertySource.getName() + "'");
         }
         Object value = propertySource.getProperty(key);
         if (value != null) {
            if (resolveNestedPlaceholders && value instanceof String) {
               value = resolveNestedPlaceholders((String) value);
            }
            logKeyFound(key, propertySource, value);
            return convertValueIfNecessary(value, targetValueType);
         }
      }
   }
   if (logger.isTraceEnabled()) {
      logger.trace("Could not find key '" + key + "' in any property source");
   }
   return null;
}

這裡的遍歷順序是從前往後,因此我們也可以得出一個結論,在PropertySources集合中靠前的Prop-ertyValue中的配置會覆蓋之後的PropertyValue中的相同屬性名配置。注意在PropertySources集合中的順序和新增時的順序沒有太大關係,即先新增不一定就在前面。

此外還值得一提的是呼叫的convertValueIfNecessary方法,在該方法中首先判斷傳入的targetType是否等等於null,如果等等於null,則直接返回。

其次獲取例項變數conversionService並賦值給conversionToUse變數,判斷conversionServiceToUse變數是否等等於null,如果不等於null,則直接呼叫其convert方法。否則首先呼叫ClassUtils的isAssign-ableValue方法(該方法主要用來判斷目標型別和值型別是否是基本型別對應的包裝型別),如果判斷成立則直接返回原值。

通過註釋我們也可以看出Spring是為了儘可能地避免去初始化共享的DefaultConversionService。還需要注意的一點是在外部化配置中,Spring沒有使用型別轉換的另一部分-基於JavaBeans的Propert-yEditor,而只使用了統一型別轉換服務-ConversionService。

// AbstractPropertyResolver#convertValueIfNecessary
protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
   if (targetType == null) {
      return (T) value;
   }
   ConversionService conversionServiceToUse = this.conversionService;
   if (conversionServiceToUse == null) {
      // Avoid initialization of shared DefaultConversionService if
      // no standard type conversion is needed in the first place...
      if (ClassUtils.isAssignableValue(targetType, value)) {
         return (T) value;
      }
      conversionServiceToUse = DefaultConversionService.getSharedInstance();
   }
   return conversionServiceToUse.convert(value, targetType);
}