Search code examples
javaspringconfigconfiguration-filesproperties-file

Issue with loading multiple properties files with Spring <util:properties /> when the list of files is a parameter


We need to load multiple properties files together and use them as one source of properties. <util:properties> allows you to pass a comma separated list of files and everything works fine so far. So, the following is good:

<util:properties loaction="conf/file1.properties,conf/file2.properties,abc-*.properties" />

However, in our case, the list of properties file is not fixed and it comes from another master properties file that is loaded before. We want to pass that list to <util:properties> as a parameter but it doesn't work.

<util:properties location="${allPropertiesFiles}" />

Where ${allPropertiesFiles} is defined as

allPropertiesFiles=conf/file1.properties,conf/file2.properties,abc-*.properties

This fails because of commas in the list of files. It treats them as one single file name and throws FileNotFoundException.

I wonder at what point Spring tries to split the files by comma and it looks like that it happens before resolving ${allPropertiesFiles}. For example if I do as below it works fine, but that is not a practical solution for us as we don't know how many files are included in that list.

<util:properties location="${propFile.location1},${propFile.location2},${propFile.location3}" />

UPDATE:

It seems to be a Spring issue with processing and splitting with ',' before resolving the property value in ${...}. I even tried using Spring EL to split it but it fails again with parsing the valid EL, because it first breaks it based on ',' then evaluates the expression. Below example fails with EL parse exception:

<util:properties location="#{'${allPropertiesFiles}'.split(',')}" />

FYI this observation is with Spring 4.2.x. Any suggestion is much appreciated.


Solution

  • So, I found a solution/workaround to my question and would like to share it here.

    The bean parsers of util:properties and context:property-placeholder split the given string value for location property by ,. It happens way before property resolution occurs. Therefore if you want to pass a comma-separated list of properties files to these beans it just won't work.

    So, instead of using <util:properties> I decided to use PropertiesFactoryBean directly and set the location as a parameter. This fixes how the bean definition is built in spring, beacuse it uses Spring default bean parser. Then I provided a custom version of Spring ResourceArrayPropertyEditor which converts a String to Resource[]. The property editor handles comma-separated strings while converting to Resource[].

    Here is my context xml now:

        <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
            <property name="customEditors">
                <map>
                    <entry key="org.springframework.core.io.Resource[]" value="com.mycompany.CustomResourceArrayPropertyEditor"/>
                </map>
            </property>
        </bean> 
        <bean id="allPropertiesFromFiles" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
                <property name="locations" value="${propertiesFile.locations}" />
        </bean>
    

    And here is my custom PropertyEditor:

    public class CustomResourceArrayPropertyEditor extends ResourceArrayPropertyEditor
    {
      private final ResourcePatternResolver resourcePatternResolver;
    
      public CustomResourceArrayPropertyEditor()
      {
        this(new PathMatchingResourcePatternResolver());
      }
    
      public CustomResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver)
      {
        super(resourcePatternResolver, null);
        this.resourcePatternResolver = resourcePatternResolver; 
      }
    
      @Override
      public void setAsText(String text)
      {
        String pattern = resolvePath(text).trim();
        String[] resourceNames = StringUtils.commaDelimitedListToStringArray(pattern);
        List<Resource> resourceList = new ArrayList<Resource>();
        try {
          for (String resourceName: resourceNames)
          {
            Resource[] resources = resourcePatternResolver.getResources(resourceName);
            for (Resource res: resources)
              resourceList.add(res);
          }
        }
        catch (IOException ex) {
          throw new IllegalArgumentException("Could not resolve resource location pattern [" + pattern + "]", ex);
        }
    
        setValue(resourceList.toArray(new Resource[0]));
      }
    }
    

    The other workaround that I could think of was to create a BeanFactoryPostProcessor to visit beans and update bean definitions of util:properties and context:propery-placeholder to simply use TypedStringValue for locations property as opposed to String[].

    Hope it helps.