[Spring Source Analysis]. properties File Reading and Placeholder ${...} Replacement Source Parsing

Keywords: Java Spring xml JDBC Attribute

Preface

We often encounter a scenario in development where some parameters in beans are fixed. In this case, we usually configure these parameters in the. properties file, and then use Spring to replace the parameters in the. properties file with placeholder "${}" when the beans are instantiated. Type read in and set to the corresponding parameters of the Bean.

The most typical way to do this is to configure JDBC. This article will study the source code of. properties file reading and placeholder "${}" replacement. First, from the code, define a DataSource, simulate four parameters of JDBC:

 1 public class DataSource {
 2 
 3     /**
 4      * Driver class
 5      */
 6     private String driveClass;
 7     
 8     /**
 9      * jdbc address
10      */
11     private String url;
12     
13     /**
14      * User name
15      */
16     private String userName;
17     
18     /**
19      * Password
20      */
21     private String password;
22 
23     public String getDriveClass() {
24         return driveClass;
25     }
26 
27     public void setDriveClass(String driveClass) {
28         this.driveClass = driveClass;
29     }
30 
31     public String getUrl() {
32         return url;
33     }
34 
35     public void setUrl(String url) {
36         this.url = url;
37     }
38 
39     public String getUserName() {
40         return userName;
41     }
42 
43     public void setUserName(String userName) {
44         this.userName = userName;
45     }
46 
47     public String getPassword() {
48         return password;
49     }
50 
51     public void setPassword(String password) {
52         this.password = password;
53     }
54 
55     @Override
56     public String toString() {
57         return "DataSource [driveClass=" + driveClass + ", url=" + url + ", userName=" + userName + ", password=" + password + "]";
58     }
59     
60 }

Define a db.properties file:

 1 driveClass=0
 2 url=1
 3 userName=2
 4 password=3

Define a properties.xml file:

 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     xmlns:aop="http://www.springframework.org/schema/aop"
 5     xmlns:tx="http://www.springframework.org/schema/tx"
 6     xsi:schemaLocation="http://www.springframework.org/schema/beans
 7         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 8         http://www.springframework.org/schema/aop
 9         http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
10 
11     <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
12         <property name="location" value="properties/db.properties"></property>
13     </bean> 
14 
15     <bean id="dataSource" class="org.xrq.spring.action.properties.DataSource">
16         <property name="driveClass" value="${driveClass}" />
17         <property name="url" value="${url}" />
18         <property name="userName" value="${userName}" />
19         <property name="password" value="${password}" />
20     </bean>
21     
22 </beans>

Write a test code:

 1 public class TestProperties {
 2 
 3     @Test
 4     public void testProperties() {
 5         ApplicationContext ac = new ClassPathXmlApplicationContext("spring/properties.xml");
 6         
 7         DataSource dataSource = (DataSource)ac.getBean("dataSource");
 8         System.out.println(dataSource);
 9     }
10     
11 }

The results are not posted. Obviously, here's how Spring reads and replaces the "${}" placeholder with the attributes in the properties file.

 

Property Placeholder Configurer Class Resolution

In the properties.xml file, we see a class PropertyPlaceholder Configurer, which, as its name implies, is an attribute placeholder configurer. Look at the inheritance diagram of this class:

The most important thing we can analyze from this graph is that PropertyPlaceholder Configurer is the implementation class of BeanFactoryPostProcessor interface. To imagine that Spring context must replace placeholders one-time by postProcessBeanFactory method after the Bean definition has been loaded and before the Bean is instantiated. "${}".

 

. properties file read source code parsing

Let's look at the postProcessBeanFactory method implementation:

 1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 2     try {
 3         Properties mergedProps = mergeProperties();
 4 
 5         // Convert the merged properties, if necessary.
 6         convertProperties(mergedProps);
 7 
 8         // Let the subclass process the properties.
 9         processProperties(beanFactory, mergedProps);
10     }
11     catch (IOException ex) {
12         throw new BeanInitializationException("Could not load properties", ex);
13     }
14 }

Follow the mergeProperties method in line 3:

 1 protected Properties mergeProperties() throws IOException {
 2     Properties result = new Properties();
 3 
 4     if (this.localOverride) {
 5         // Load properties from file upfront, to let local properties override.
 6         loadProperties(result);
 7     }
 8 
 9     if (this.localProperties != null) {
10         for (Properties localProp : this.localProperties) {
11             CollectionUtils.mergePropertiesIntoMap(localProp, result);
12         }
13     }
14 
15     if (!this.localOverride) {
16         // Load properties from file afterwards, to let those properties override.
17         loadProperties(result);
18     }
19 
20     return result;
21 }

Line 2's method, new, produces a property called result, which is passed in with subsequent code, and the data in the. properties file is written to the result.

OK, then look at the way the code goes to line 17, loading the. properties file through the file:

 1 protected void loadProperties(Properties props) throws IOException {
 2     if (this.locations != null) {
 3         for (Resource location : this.locations) {
 4             if (logger.isInfoEnabled()) {
 5                 logger.info("Loading properties file from " + location);
 6             }
 7             InputStream is = null;
 8             try {
 9                 is = location.getInputStream();
10 
11                 String filename = null;
12                 try {
13                     filename = location.getFilename();
14                 } catch (IllegalStateException ex) {
15                     // resource is not file-based. See SPR-7552.
16                 }
17 
18                 if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
19                     this.propertiesPersister.loadFromXml(props, is);
20                 }
21                 else {
22                     if (this.fileEncoding != null) {
23                         this.propertiesPersister.load(props, new InputStreamReader(is, this.fileEncoding));
24                     }
25                     else {
26                         this.propertiesPersister.load(props, is);
27                     }
28                 }
29             }
30             catch (IOException ex) {
31                 if (this.ignoreResourceNotFound) {
32                     if (logger.isWarnEnabled()) {
33                         logger.warn("Could not load properties from " + location + ": " + ex.getMessage());
34                     }
35                 }
36                 else {
37                     throw ex;
38                 }
39             }
40             finally {
41                 if (is != null) {
42                     is.close();
43                 }
44             }
45         }
46     }
47 }

Line 9, the configuration of Property Placeholder Configurer can be passed into the path list (of course, only one db.properties is passed here), line 3 traverses the list, line 9 obtains the binary data corresponding to. properties through an input byte stream InputStream, and the code in line 23 parses the binary in InputStream and writes it into it. In the first parameter Properties, Properties is JDK's native tool for reading. properties files.

In this simple process, the data in. Properties is analyzed and written into result (result is a new property in merge Properties method).

 

Placeholder "${...}" replaces source code parsing

See above. properties file reading process, then you should replace the "${}" placeholder, or go back to the postProcessBeanFactory method:

 1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 2     try {
 3         Properties mergedProps = mergeProperties();
 4 
 5         // Convert the merged properties, if necessary.
 6         convertProperties(mergedProps);
 7 
 8         // Let the subclass process the properties.
 9         processProperties(beanFactory, mergedProps);
10     }
11     catch (IOException ex) {
12         throw new BeanInitializationException("Could not load properties", ex);
13     }
14 }

Line 3 merges the. properties file (called merge because multiple. properties files may have the same Key).

Line 6 converts the merged Proerties if necessary, and it doesn't see any use.

The placeholder "${...}" is replaced in line 9. It should be stated beforehand that the postProcessBeanFactory method call of the BeanFactoryPostProcessor class is parsed after the Bean definition is parsed, so all Bean definitions are already available in the current beanFactory parameter, which should be clear to friends who are familiar with the Bean parsing process. . Follow the processProperties method in line 9:

 1 protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
 2         throws BeansException {
 3 
 4     StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props);
 5     BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
 6 
 7     String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
 8     for (String curName : beanNames) {
 9         // Check that we're not parsing our own bean definition,
10         // to avoid failing on unresolvable placeholders in properties file locations.
11         if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
12             BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
13             try {
14                 visitor.visitBeanDefinition(bd);
15             }
16             catch (Exception ex) {
17                 throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage());
18             }
19         }
20     }
21 
22     // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
23     beanFactoryToProcess.resolveAliases(valueResolver);
24 
25     // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
26     beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
27 }

Line 4 shows a Placeholder Resolving String Value Resolver and passes in Properties, which, as the name implies, is a string value parser that holds the. properties file configuration.

Line 5 Bean Definition Vistor is passed into StringValue Resolver above. As the name implies, this is a Bean Definition Access Tool, which holds a string value parser. Imagine accessing the Bean definition through Bean Definition Vistor and using the StringValue Resolver passed in by the constructor when encountering a string that needs to be parsed. Character string.

Line 7 retrieves the names of all Bean definitions through BeanFactory.

Line 8 begins traversing the names of all Bean definitions, noting the first judgment in line 11, "!" (curName.equals(this.beanName),"which means Property Placeholder Configurer, meaning that Property Placeholder Configurer itself does not resolve placeholder"${...}".

Focusing on 14 lines of code, BeanDefinition Vistor's visitBeanDefinition method is passed into BeanDefinition:

 1 public void visitBeanDefinition(BeanDefinition beanDefinition) {
 2     visitParentName(beanDefinition);
 3     visitBeanClassName(beanDefinition);
 4     visitFactoryBeanName(beanDefinition);
 5     visitFactoryMethodName(beanDefinition);
 6     visitScope(beanDefinition);
 7     visitPropertyValues(beanDefinition.getPropertyValues());
 8     ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
 9     visitIndexedArgumentValues(cas.getIndexedArgumentValues());
10     visitGenericArgumentValues(cas.getGenericArgumentValues());
11 }

You can see that this method visits parent, class, factory-bean, factory-method, scope, property, constructor-arg attributes in the definition of <bean> in turn, but parses'${...}'whenever needed. Here we parse the "${...}" in the property tag, so follow the code in line 7:

1 protected void visitPropertyValues(MutablePropertyValues pvs) {
2     PropertyValue[] pvArray = pvs.getPropertyValues();
3     for (PropertyValue pv : pvArray) {
4         Object newVal = resolveValue(pv.getValue());
5         if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
6             pvs.add(pv.getName(), newVal);
7         }
8     }
9 }

Get the array of attributes for traversal. Line 4 of the code parses the value of attributes to get the new value of attributes. Line 5 judges that the new value of attributes differs from the original value of attributes. Line 6 of the code replaces the original value of attributes with the new value of attributes. So follow the resolveValue method in line 4:

 1 protected Object resolveValue(Object value) {
 2     if (value instanceof BeanDefinition) {
 3         visitBeanDefinition((BeanDefinition) value);
 4     }
 5     else if (value instanceof BeanDefinitionHolder) {
 6         visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
 7     }
 8     else if (value instanceof RuntimeBeanReference) {
 9         RuntimeBeanReference ref = (RuntimeBeanReference) value;
10         String newBeanName = resolveStringValue(ref.getBeanName());
11         if (!newBeanName.equals(ref.getBeanName())) {
12             return new RuntimeBeanReference(newBeanName);
13         }
14     }
15     else if (value instanceof RuntimeBeanNameReference) {
16         RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value;
17         String newBeanName = resolveStringValue(ref.getBeanName());
18         if (!newBeanName.equals(ref.getBeanName())) {
19             return new RuntimeBeanNameReference(newBeanName);
20         }
21     }
22     else if (value instanceof Object[]) {
23         visitArray((Object[]) value);
24     }
25     else if (value instanceof List) {
26         visitList((List) value);
27     }
28     else if (value instanceof Set) {
29         visitSet((Set) value);
30     }
31     else if (value instanceof Map) {
32         visitMap((Map) value);
33     }
34     else if (value instanceof TypedStringValue) {
35         TypedStringValue typedStringValue = (TypedStringValue) value;
36         String stringValue = typedStringValue.getValue();
37         if (stringValue != null) {
38             String visitedString = resolveStringValue(stringValue);
39             typedStringValue.setValue(visitedString);
40         }
41     }
42     else if (value instanceof String) {
43         return resolveStringValue((String) value);
44     }
45     return value;
46 }

Here we mainly make a judgment on the value type. We configure the string in the configuration file, so we can look at the string-related code, that is, 34 lines of judgment. The rest is almost the same. You can see how the source code does. Lines 35 to 36 get the attribute value. Line 38 resolveStringValue parses the string:

1 protected String resolveStringValue(String strVal) {
2     if (this.valueResolver == null) {
3         throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
4                 "object into the constructor or override the 'resolveStringValue' method");
5     }
6     String resolvedValue = this.valueResolver.resolveStringValue(strVal);
7     // Return original String if not modified.
8     return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
9 }

Continuing with the method in line 6, valueResolver, as mentioned earlier, is an imported Placeholder Resolving StringValue Resolver. Take a look at the resolveStringValue method implementation:

 1 public String resolveStringValue(String strVal) throws BeansException {
 2     String value = this.helper.replacePlaceholders(strVal, this.resolver);
 3     return (value.equals(nullValue) ? null : value);
 4 }

Line 2's replacePlaceholders method, as the name implies, replaces placeholders, which are located in the PropertyPlaceholder Helper class. Follow this method:

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

Continue with the parseStringValue method in line 3, which traces to the core code for replacing placeholders:

 1 protected String parseStringValue(
 2         String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
 3 
 4     StringBuilder buf = new StringBuilder(strVal);
 5 
 6     int startIndex = strVal.indexOf(this.placeholderPrefix);
 7     while (startIndex != -1) {
 8         int endIndex = findPlaceholderEndIndex(buf, startIndex);
 9         if (endIndex != -1) {
10             String placeholder = buf.substring(startIndex + this.placeholderPrefix.length(), endIndex);
11             if (!visitedPlaceholders.add(placeholder)) {
12                 throw new IllegalArgumentException(
13                         "Circular placeholder reference '" + placeholder + "' in property definitions");
14             }
15             // Recursive invocation, parsing placeholders contained in the placeholder key.
16             placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
17 
18             // Now obtain the value for the fully resolved key...
19             String propVal = placeholderResolver.resolvePlaceholder(placeholder);
20             if (propVal == null && this.valueSeparator != null) {
21                 int separatorIndex = placeholder.indexOf(this.valueSeparator);
22                 if (separatorIndex != -1) {
23                     String actualPlaceholder = placeholder.substring(0, separatorIndex);
24                     String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
25                     propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
26                     if (propVal == null) {
27                         propVal = defaultValue;
28                     }
29                 }
30             }
31             if (propVal != null) {
32                 // Recursive invocation, parsing placeholders contained in the
33                 // previously resolved placeholder value.
34                 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
35                 buf.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
36                 if (logger.isTraceEnabled()) {
37                     logger.trace("Resolved placeholder '" + placeholder + "'");
38                 }
39                 startIndex = buf.indexOf(this.placeholderPrefix, startIndex + propVal.length());
40             }
41             else if (this.ignoreUnresolvablePlaceholders) {
42                 // Proceed with unprocessed value.
43                 startIndex = buf.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
44             }
45             else {
46                 throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'");
47             }
48 
49             visitedPlaceholders.remove(placeholder);
50         }
51         else {
52             startIndex = -1;
53         }
54     }
55 
56     return buf.toString();
57 }

Take a look at this process:

  1. Get the location index startIndex of placeholder prefix "${"
  2. The placeholder prefix "${" exists. Start after "${" to get the placeholder suffix "}" position index endIndex.
  3. If the placeholder prefix location index startIndex and placeholder suffix location index endIndex exist, intercept the middle part placeHolder
  4. Get the value propVal corresponding to placeHolder from Properties
  5. If propVal does not exist, try to partition placeHolder with ":" once. If the partition yields results, then the former part is named actualPlaceholder, and the latter part is named defaultValue. Try to get the corresponding value of actualPlaceholder from Properties, and if it exists, take this value, if it does not exist. In the case of defaultValue, the final value is assigned to propVal.
  6. Return propVal, which is the value after replacement

The process is very long. Through this whole process, the contents of the placeholder "${...}" are replaced with the values we need.

Posted by onlyican on Thu, 04 Jul 2019 13:48:55 -0700