I encountered a un-expected behavior on configuration properties feature since Spring Boot 2.x. On 1.5.x, it work fine.

Conditions

  • Define a property of Resource[] on custom configuration properties class
  • Specify a property value using classpath*: prefix such as classpath*:files/*.txt

Expected result

  • Binding multiple Resource instance that matches pattern string

Actual result

  • Binding a single Resource that holds a specify pattern string such as classpath*:files/*.txt

Repro test

demo.zip

package com.example.demoresources;

import org.assertj.core.api.Assertions;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "my.files=classpath*:files/*.txt")
@EnableConfigurationProperties({DemoResourcesApplicationTests.MyProperties.class})
public class DemoResourcesApplicationTests {

  @Autowired
  MyProperties myProperties;

  @BeforeClass
  public static void setup() throws IOException {
    Path dir = Paths.get("target", "test-classes", "files");
    Files.createDirectories(dir);
    createFileIfNotExist(dir.resolve("a.txt"));
    createFileIfNotExist(dir.resolve("b.txt"));
  }

  private static void createFileIfNotExist(Path path) throws IOException {
    if (!path.toFile().exists()) {
      Files.createFile(path);
    }
  }

  @Test
  public void contextLoads() {
    Resource[] files = myProperties.getFiles();
    List<String> fileNames = Arrays.stream(files).map(Resource::getFilename).collect(Collectors.toList());
    Assertions.assertThat(fileNames)
        .hasSize(2)
        .contains("a.txt", "b.txt"); // Success with Spring Boot 1.5.x but fail with Spring Boot 2.x ...
  }

  @ConfigurationProperties(prefix = "my")
  public static class MyProperties {
    private Resource[] files = {};

    public void setFiles(Resource[] files) {
      this.files = files;
    }

    public Resource[] getFiles() {
      return files;
    }

  }

}

Comment From: wilkinsona

Thanks for the sample. There's some similarity to the problem described in #12166.

In 1.5, the conversion is handled by Framework's ResourceArrayPropertyEditor. In 2.x, the conversion is handled by Boot's DelimitedStringToArrayConverter. ResourceArrayPropertyEditor is registered with Boot's TypeConverterConversionService but TypeConverterConverter is only registered with a single convertible pair of java.lang.String -> java.lang.Object which does not match the java.lang.String -> [Lorg.springframework.core.io.Resource; convertible pair for the String to Resource[] conversion.

Here's an addition to ArrayBinderTests that reproduces the problem:

@Test
public void bindToResourceArrayShouldUsePropertyEditorAndPatternResolution() {
    MockConfigurationPropertySource source = new MockConfigurationPropertySource();
    source.put("foo", "classpath*:/**/*.class");
    this.sources.add(source);
    assertThat(this.binder.bind("foo", Bindable.of(Resource[].class)).get().length)
            .isGreaterThan(1);
}

Binding to a collection exhibits the same problem.

Comment From: philwebb

I'm not totally sure that we should try and support pattern expansion in the same way as the ResourceEditor. I'm a little uneasy about performing such logic during the binding process. I wonder if binding the pattern as a String then using that later against a ResourcePatternResolver might be a better approach in most situations?

Comment From: wilkinsona

We're going to deal with this on a case-by-case basis. We'll add only the resource array and resource collection property editors for now.

Comment From: mbhave

The Spring Framework @Value behavior is inconsistent for resource arrays vs collections. We should hold off on a change in Boot until we know what Framework might do.

Comment From: filiphr

Just to add some more info from https://github.com/spring-projects/spring-boot/issues/12993#issuecomment-385172824. When using Resource[] with @Value it works correctly.

For example doing:

@Value("${dummy.dummy-files}")
private Resource[] resources;

Comment From: wilkinsona

Thanks, @filiphr. We're already aware that Resource[] works with @Value. The Framework issue that Madhura opened notes this and aims to address the fact that it does not work with Collection<Resource>. We'd like that inconsistency to be addressed one way or another before deciding how to proceed in Boot.

Comment From: filiphr

Thanks for clearing it up @wilkinsona. I was only trying to bring some more information from the other issue (although I have no doubt that you are on top of it 😄).

Comment From: wilkinsona

The Framework issue has moved into the 5.x backlog so we're unlikely to be able to tackle this in 2.1.x or even 2.2.x.

Comment From: LeoFuso

Hello team, any news about this issue?

Comment From: snicoll

@LeoFuso nothing that wouldn't be available here. As noted above, we've opened a Spring Framework issue that still unresolved, see https://github.com/spring-projects/spring-boot/issues/15835#issuecomment-507360663

Comment From: vermgit

I think this is related and has a work around in the question.

Comment From: wilkinsona

It looks like there's going to be a change in Framework 6.1. We should see what, if anything, we can do in Boot 3.2.

Comment From: wilkinsona

Framework now consistently converts a pattern to multiple resources whether it's a Resource[] or a Collection<Resource>. For consistency, we should do the same in Boot for binding. We'll need to figure out the best way to do that with the conversion service used by the binder.