1. 程式人生 > >Spring的property-placeholder原理分析

Spring的property-placeholder原理分析

一、解析封裝property-placeholder相關的BeanDefinition

有兩種方式: (1)在XML配置格式如下

	<context:property-placeholder location="classpath*:xxxx.properties"/>

(2)註解配置如下:

@Configuration
@PropertySource("classpath:xxx.properties")
public class PropertiesWithJavaConfig {
 
   @Bean
   public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer
() { return new PropertySourcesPlaceholderConfigurer(); } }

java類中

	@Value( "${jdbc.url}" )
	private String jdbcUrl;

檢視spring-context原始碼模組下的spring.handlers檔案可以得知其解析的類為如下

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

進入ContextNamespaceHandler類中可以看到解析property-placeholder標籤委託給了PropertyPlaceholderBeanDefinitionParser類

	registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());

PropertyPlaceholderBeanDefinitionParser類繼承圖如下: PropertyPlaceholderBeanDefinitionParser

解析自定義標籤最終是呼叫BeanDefinitionParser的parse方法的,其在AbstractBeanDefinitionParser中有實現

	public final BeanDefinition parse(Element element, ParserContext parserContext)
{ //呼叫parseInternal進行解析 AbstractBeanDefinition definition = parseInternal(element, parserContext); ... return definition; }

parseInternal方法在AbstractSingleBeanDefinitionParser類中實現

	protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
		//拿到父節點名稱
		String parentName = getParentName(element);
		if (parentName != null) {
			builder.getRawBeanDefinition().setParentName(parentName);
		}
		//【標記1】拿到bean的型別
		Class<?> beanClass = getBeanClass(element);
		if (beanClass != null) {
			builder.getRawBeanDefinition().setBeanClass(beanClass);
		}
		else {
			//拿到類名
			String beanClassName = getBeanClassName(element);
			if (beanClassName != null) {
				builder.getRawBeanDefinition().setBeanClassName(beanClassName);
			}
		}
		...
		//【標記2】解析標籤
		doParse(element, parserContext, builder);
		return builder.getBeanDefinition();
	}

Spring會把XML配置檔案中配置的bean封裝成BeanDefinition,用於後面的例項化。而上面是通過BeanDefinitionBuilder來構建一個BeanDefinition進行返回。

【標記1】拿到bean的型別

先看看上面【標記1】的地方,那裡決定了返回的BeanDefinition具體型別是什麼。PropertyPlaceholderBeanDefinitionParser類覆寫了該方法。

	protected Class<?> getBeanClass(Element element) {
		//3.1及之後版本的xsd檔案預設值是這個
		if (SYSTEM_PROPERTIES_MODE_DEFAULT.equals(element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE))) {
			return PropertySourcesPlaceholderConfigurer.class;
		}
		//3.0或之前版本的xsd返回這個
		return PropertyPlaceholderConfigurer.class;
	}

返回結果和spring-context.xsd有關係,3.0或之前版本返回PropertyPlaceholderConfigurer.class,3.1及之後版本返回PropertySourcesPlaceholderConfigurer.class。

【標記2】解析標籤 PropertyPlaceholderBeanDefinitionParser類覆寫了該方法

	protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
		//呼叫父類解析一部分標籤屬性
		super.doParse(element, parserContext, builder);
		builder.addPropertyValue("ignoreUnresolvablePlaceholders",
				Boolean.valueOf(element.getAttribute("ignore-unresolvable")));
		String systemPropertiesModeName = element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE);
		if (StringUtils.hasLength(systemPropertiesModeName) &&
				!systemPropertiesModeName.equals(SYSTEM_PROPERTIES_MODE_DEFAULT)) {
			//3.1及之後的版本進這裡
			builder.addPropertyValue("systemPropertiesModeName", "SYSTEM_PROPERTIES_MODE_" + systemPropertiesModeName);
		}
		if (element.hasAttribute("value-separator")) {
			builder.addPropertyValue("valueSeparator", element.getAttribute("value-separator"));
		}
		if (element.hasAttribute("trim-values")) {
			builder.addPropertyValue("trimValues", element.getAttribute("trim-values"));
		}
		if (element.hasAttribute("null-value")) {
			builder.addPropertyValue("nullValue", element.getAttribute("null-value"));
		}
	}

doParse方法主要解析配置的相關屬性

二、PropertySourcesPlaceholderConfigurer類分析

上面介紹了Spring如何解析XML配置中的context:property-placeholder標籤封裝成BeanDefinition放入容器中。本人寫的時候看的是Spring5的原始碼,所以最後解析該標籤得到的BeanDefinition的類的型別是PropertySourcesPlaceholderConfigurer。接下來分析這個類究竟做了什麼。 PropertySourcesPlaceholderConfigurer

可以看到PropertySourcesPlaceholderConfigurer間接實現了BeanFactoryPostProcessor介面,該介面是在Spring解析完所有bean並封裝成BeanDefinition的方法在PropertySourcesPlaceholderConfigurer類中實現了

	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		//propertySources為空,就會去載入environment中的屬性、和XML中指定的檔案中的屬性
		if (this.propertySources == null) {
			this.propertySources = new MutablePropertySources();
			//新增environment中的屬性
			if (this.environment != null) {//因為實現了EnvironmentAware介面,environment在該介面方法中賦值
				this.propertySources.addLast(
					new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
						@Override
						@Nullable
						public String getProperty(String key) {
							return this.source.getProperty(key);
						}
					}
				);
			}
			try {
				//【標記3】進行載入XML中的location屬性的檔案,併合並props屬性
				PropertySource<?> localPropertySource =
						new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
				if (this.localOverride) {
					//允許本地覆蓋,就加在前面,優先順序高
					this.propertySources.addFirst(localPropertySource);
				}
				else {
					//不允許本地覆蓋,就加在後面,優先順序低
					this.propertySources.addLast(localPropertySource);
				}
			}
			catch (IOException ex) {
				throw new BeanInitializationException("Could not load properties", ex);
			}
		}
		//【標記4】處理容器中的BeanDefinition
		processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
		//賦值成員變數
		this.appliedPropertySources = this.propertySources;
	}

上面可以看到,如果propertySources沒有被賦值,那就會去載入environment中的屬性、和XML中指定的檔案中的屬性,最後就把propertySources傳到PropertySourcesPropertyResolver物件,開始處理容器中的BeanDefinition中需要替換的屬性

【標記3】進行載入XML中的location屬性的檔案,併合並props屬性

	protected Properties mergeProperties() throws IOException {
		Properties result = new Properties();
		//localOverride預設為false,表示本地屬性的不能覆蓋從外部載入屬性
		if (this.localOverride) {
			// 載入location屬性的表示的檔案,本質是通過java的Properties類進行載入
			loadProperties(result);
		}
		//把本地的屬性合併,localProperties可在xml檔案中通過props屬性進行設定
		if (this.localProperties != null) {
			for (Properties localProp : this.localProperties) {
				CollectionUtils.mergePropertiesIntoMap(localProp, result);
			}
		}
		if (!this.localOverride) {
			// 載入location屬性的表示的檔案,本質是通過java的Properties類進行載入
			loadProperties(result);
		}
		return result;
	}

邏輯比較簡單,localOverride變數決定是否本地的屬性可以覆蓋另外載入的屬性

【標記4】處理容器中的BeanDefinition

	protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
			final ConfigurablePropertyResolver propertyResolver) throws BeansException {
		//新增替換的字首,預設是 ${
		propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
		//新增替換的字尾,預設是 } 
		propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
		//新增預設值分隔符,預設是 :
		propertyResolver.setValueSeparator(this.valueSeparator);
		//新建一個處理器,替換邏輯就在這
		StringValueResolver valueResolver = strVal -> {
			//ignoreUnresolvablePlaceholders預設為false
			String resolved = (this.ignoreUnresolvablePlaceholders ?
					propertyResolver.resolvePlaceholders(strVal) :
					propertyResolver.resolveRequiredPlaceholders(strVal));
			//trimValues預設為false
			if (this.trimValues) {
				resolved = resolved.trim();
			}
			//如果設定了nullValue,則如果和nullValue相等,返回null
			return (resolved.equals(this.nullValue) ? null : resolved);
		};
		//開始進行處理
		doProcessProperties(beanFactoryToProcess, valueResolver);
	}

上面的StringValueResolver是文字替換處理器,實質就是它來把${xx}這些經過處理返回真正的值。

	protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
			StringValueResolver valueResolver) {
		//把處理器封裝到BeanDefinitionVisitor
		BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
		//拿到所有bean名字
		String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
		for (String curName : beanNames) {
			//處理除了自身以外的bean
			if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
				BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
				try {
					//開始處理
					visitor.visitBeanDefinition(bd);
				}
				catch (Exception ex) {
					throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
				}
			}
		}

		//處理別名
		beanFactoryToProcess.resolveAliases(valueResolver);

		//把valueResolver傳遞給beanFactoryToProcess,供其用於處理註解的解析等
		beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
	}

遍歷容器中的所有BeanDefinition進行處理,把文字處理器又封裝進了BeanDefinitionVisitor,所以其visitBeanDefinition方法是真正開始處理BeanDefinition的屬性。在方法最後還對別名進行了處理,然後把valueResolver傳遞給beanFactoryToProcess,供其用於處理註解的解析等

	public void visitBeanDefinition(BeanDefinition beanDefinition) {
		//處理父類名
		visitParentName(beanDefinition);
		//處理類名
		visitBeanClassName(beanDefinition);
		//處理工廠bean名
		visitFactoryBeanName(beanDefinition);
		//處理工廠方法名
		visitFactoryMethodName(beanDefinition);
		//處理生命週期
		visitScope(beanDefinition);
		//處理屬性值
		if (beanDefinition.hasPropertyValues()) {
			visitPropertyValues(beanDefinition.getPropertyValues());
		}
		//處理構造方法值
		if (beanDefinition.hasConstructorArgumentValues()) {
			ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
			visitIndexedArgumentValues(cas.getIndexedArgumentValues());
			visitGenericArgumentValues(cas.getGenericArgumentValues());
		}
	}

上面可以看到:visitBeanDefinition對beanDefinition的一些屬性進行了處理

上面對beanDefinition的處理最終還是會呼叫StringValueResolver的resolveStringValue方法(上面用了java8新增的語法),而最終呼叫的是PropertySourcesPropertyResolver的resolvePlaceholders或resolveRequiredPlaceholders方法(取決於ignoreUnresolvablePlaceholders變數)。下面來挑選resolveRequiredPlaceholders方法來分析

	public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
		if (this.strictHelper == null) {
			//建立一個PropertyPlaceholderHelper,引數傳false
			this.strictHelper = createPlaceholderHelper(false);
		}
		return doResolvePlaceholders(text, this.strictHelper);
	}

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

PropertyPlaceholderHelper是個工具類

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

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

上面可以看到傳遞了getPropertyAsRawString方法(java8語法),而該方法是返回key對應的屬性值

	protected String parseStringValue(
			String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
		StringBuilder result = new StringBuilder(value);
		int startIndex = value.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;
				//這裡新增不進去Set集合,說明存在迴圈引用
				if (!visitedPlaceholders.add(originalPlaceholder)) {
					throw new IllegalArgumentException(
							"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
				}
				//遞迴解析,拿到最終值?
				placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
				//呼叫處理器拿到屬性值,其實呼叫的是getPropertyAsRawString方法
				String propVal = placeholderResolver.resolvePlaceholder(placeholder);
				if (propVal == null && this.valueSeparator != null) {
					//為空,可能格式是  xxxx:xxx(冒號後面是預設值)
					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) {
					// Proceed with unprocessed value.
					//如果ignoreUnresolvablePlaceholders為true,說明忽略無法解析的${xxx}
					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();
	}

程式碼雖然有點長,但邏輯還是很清晰

  1. 先拿到要替換的值(即:${xx}中的xx)
  2. 遞迴解析,拿到真正要替換的值(即有可能是KaTeX parse error: Expected '}', got 'EOF' at end of input: {{}/xx}這樣的)
  3. 呼叫placeholderResolver的resolvePlaceholder方法拿到屬性值(其實就會呼叫上面傳遞的getPropertyAsRawString方法)
  4. 如果屬性值為空,可能是有預設值分隔符(即有:),一番處理後拿到屬性值
  5. 對屬性值進行遞迴解析,拿到最終屬性值(即可能是在屬性檔案中寫法:xxx=${xxx},一般是多個屬性檔案的情況)
  6. 最終屬性值不為空就替換,否則根據 ignoreUnresolvablePlaceholders判斷是否拋異常還是繼續解析
  7. 把解析成功的要替換的值從visitedPlaceholders中移除
  8. 繼續迴圈或返回結果