Hello friends. While upgrading to spring-boot v3.2.0, I believe I found a regression in the binding of properties to a List<org.springframework.core.io.Resource>. A small sample will demonstrate:
@SpringBootApplication
@EnableConfigurationProperties(CoolConfigProperties.class)
public class SampleApp {
public static void main(String[] args) {
SpringApplication.run(SampleApp.class, args);
}
}
@ConfigurationProperties(prefix = "cool")
record CoolConfigProperties(List<Resource> resources) {}
For this example, we will set an environment variable COOL_RESOURCES=classpath:foo.properties,file:./bar.properties.
Resources are bound to ConfigurationProperties as expected when running with spring-boot v3.1.6:
However spring-boot v3.2.0 will fail to start:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
2023-11-26T23:17:14.625-05:00 INFO 32744 --- [ main] com.example.SampleApp : Starting SampleApp using Java 21.0.1 with PID 32744 (C:\Users\groat\Desktop\list-property-sample\target\classes started by groat in C:\Users\groat\Desktop\list-property-sample)
2023-11-26T23:17:14.628-05:00 INFO 32744 --- [ main] com.example.SampleApp : No active profile set, falling back to 1 default profile: "default"
2023-11-26T23:17:14.854-05:00 WARN 32744 --- [ main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'cool-com.example.CoolConfigProperties': Could not bind properties to 'CoolConfigProperties' : prefix=cool, ignoreInvalidFields=false, ignoreUnknownFields=true
2023-11-26T23:17:14.857-05:00 INFO 32744 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-11-26T23:17:14.866-05:00 ERROR 32744 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'cool.resources' to java.util.List<org.springframework.core.io.Resource>:
Property: cool.resources
Value: "classpath:/foo.properties,file:./bar.properties"
Origin: System Environment Property "COOL_RESOURCES"
Reason: failed to convert java.lang.String to java.util.List<org.springframework.core.io.Resource> (caused by java.lang.IllegalArgumentException: Cannot convert value of type 'java.lang.String' to required type 'java.util.List': PropertyEditor [org.springframework.core.io.support.ResourceArrayPropertyEditor] returned inappropriate value of type 'org.springframework.core.io.ClassPathResource')
Action:
Update your application's configuration
Process finished with exit code 1
Comment From: quaff
It's a spring boot regression introduced by https://github.com/spring-projects/spring-boot/commit/0e3a196af5c58954cfa7d3920b5782125ef6a1ff.
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
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.ClassPathResource;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
@EnableConfigurationProperties(ResourcesInjectionTests.ConfigProperties.class)
@TestPropertySource(properties = "resources=classpath:foo.properties,file:./bar.properties")
public class ResourcesInjectionTests {
@Autowired
ConfigProperties configProperties;
@Test
void test() {
assertThat(configProperties.resources).hasSize(2);
assertThat(configProperties.resources.get(0)).isInstanceOf(ClassPathResource.class);
assertThat(configProperties.resources.get(1)).isInstanceOf(FileUrlResource.class);
}
@ConfigurationProperties
record ConfigProperties(List<Resource> resources) {
}
}
This test case works fine with 3.1.6 but failed with 3.2.0.
Comment From: wilkinsona
There's also a related problem with Resource[] binding.
With Spring Boot 3.1.6 you get [class path resource [foo.properties], URL [file:bar.properties]] but with 3.2.0 you get [class path resource [foo.properties,file:bar.properties]]. With Spring Boot 3.1.6 and Framework overridden to 6.1.1 you still get [class path resource [foo.properties], URL [file:bar.properties]]. There may be a Boot problem here as well as a Framework problem (https://github.com/spring-projects/spring-framework/issues/31693).
Comment From: gznglor
This is the most recent issue about binding, so commenting here... Is my issue related or is it separate?
I recently upgraded to Spring Boot 3.2.0 to fix some vulnerabilities, and now my parameters with underscores aren't binding to my object.
With Spring Boot 3.1.6, all of my parameters successfully binded (although technically, I have to write my own setter to set the parameters with underscores). This has worked for as long as I can remember. But in 3.2.0, my setter is completely skipped.
Below works in 3.1.6 but not 3.2.0:
@GetMapping
@ResponseStatus(HttpStatus.OK)
fun get(
mySearchParameters: MySearchParameters,
pageable: Pageable
): List<MyStuff> { ... }
data class MySearchParameters(
var name: String? = null,
var userCode: Long? = null
) {
fun setUser_code(userCode: Long?) {
this.userCode = userCode
}
}
In 3.2.0, the variable name is binding as expected, but userCode is not binding.
Comment From: wilkinsona
@gznglor that seems to be completely unrelated to binding a List<Resource>. Please open a separate issue, providing a complete yet minimal sample that reproduces the problem.
Comment From: quaff
There's also a related problem with
Resource[]binding.With Spring Boot 3.1.6 you get
[class path resource [foo.properties], URL [file:bar.properties]]but with 3.2.0 you get[class path resource [foo.properties,file:bar.properties]]. With Spring Boot 3.1.6 and Framework overridden to 6.1.1 you still get[class path resource [foo.properties], URL [file:bar.properties]]. There may be a Boot problem here as well as a Framework problem (spring-projects/spring-framework#31693).
It's fixed by https://github.com/spring-projects/spring-framework/pull/31700.
Comment From: jagneray
Hello same probleme here :
I have two databases mongoDb, and in version 3.1.5 everything was fine and now i have this error :
Failed to bind properties under 'spring.data.mongodb' to com.adeo.sofi.configuration.CustomMongoProperties$MongoDbProperties:
Here my configuration
data:
mongodb:
sofi:
database: ***
uri: ****
referential:
database: ***
uri: ****
Comment From: wilkinsona
@jagneray without knowing anything about your CustomMongoProperties$MongoDbProperties class, it's not clear to me how your snippet of YAML relates to this issue. I can't see anything that looks like it would be binding a List<Resource>.
Comment From: wilkinsona
Here's a minimal example that tries to capture the status of things both for the case described above and for the wildcard case that https://github.com/spring-projects/spring-boot/issues/15835 sought to address:
package com.example.gh38556;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.Resource;
import com.example.gh38556.Gh38556Application.CoolConfigProperties;
@SpringBootApplication
@EnableConfigurationProperties(CoolConfigProperties.class)
public class Gh38556Application {
@Value("${cool.list}")
private List<Resource> list;
@Value("${cool.array}")
private Resource[] array;
@Value("${cool.wildcard-list}")
private List<Resource> wildcardList;
@Value("${cool.wildcard-array}")
private Resource[] wildcardArray;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Gh38556Application.class,
"--cool.list=classpath:foo.properties,file:./bar.properties",
"--cool.array=classpath:foo.properties,file:./bar.properties",
"--cool.wildcard-list=classpath*:com/example/**/*.class",
"--cool.wildcard-array=classpath*:com/example/**/*.class");
CoolConfigProperties coolConfigProperties = context.getBean(CoolConfigProperties.class);
System.out.println("@ConfigurationProperties:");
System.out.println(" " + coolConfigProperties.list());
System.out.println(" " + Arrays.toString(coolConfigProperties.array()));
System.out.println(" " + coolConfigProperties.wildcardList());
System.out.println(" " + Arrays.toString(coolConfigProperties.wildcardArray()));
Gh38556Application app = context.getBean(Gh38556Application.class);
System.out.println("@Value:");
System.out.println(" " + app.list);
System.out.println(" " + Arrays.toString(app.array));
System.out.println(" " + app.wildcardList);
System.out.println(" " + Arrays.toString(app.wildcardArray));
}
@ConfigurationProperties(prefix = "cool")
record CoolConfigProperties(List<Resource> list, Resource[] array,
List<Resource> wildcardList, Resource[] wildcardArray) {}
}
With Spring Boot 3.1.6, the following is output:
@ConfigurationProperties:
[class path resource [foo.properties], URL [file:./bar.properties]]
[class path resource [foo.properties], URL [file:./bar.properties]]
[class path resource [classpath*:com/example/**/*.class]]
[class path resource [classpath*:com/example/**/*.class]]
@Value:
[class path resource [foo.properties,file:bar.properties]]
[class path resource [foo.properties,file:bar.properties]]
[class path resource [classpath*:com/example/**/*.class]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
With Spring Boot 3.2.1-SNAPSHOT and Spring Framework 6.1.2-SNAPSHOT, the following is output:
@ConfigurationProperties:
[class path resource [foo.properties,file:bar.properties]]
[class path resource [foo.properties,file:bar.properties]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
@Value:
[class path resource [foo.properties,file:bar.properties]]
[class path resource [foo.properties,file:bar.properties]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
The behavior of @ConfigurationProperties and @Value is now consistent. We have, however, lost the support for comma-separated values being automatically expanded into multiple resources. https://github.com/spring-projects/spring-framework/pull/31700 should address that but it remains to be seen in what version of Framework that change could be made.
Comment From: wilkinsona
@snicoll @jhoeller do you have a feel for if and when https://github.com/spring-projects/spring-framework/pull/31700 might land?
Comment From: snicoll
@wilkinsona this should be available in Spring Framework 6.1.2-SNAPSHOT.
Comment From: wilkinsona
With the latest 6.1.2-SNAPSHOT, I now see the following:
@ConfigurationProperties:
[class path resource [foo.properties], URL [file:bar.properties]]
[class path resource [foo.properties], URL [file:bar.properties]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
@Value:
[class path resource [foo.properties], URL [file:bar.properties]]
[class path resource [foo.properties], URL [file:bar.properties]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
[file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application$CoolConfigProperties.class], file [/Users/awilkinson/dev/temp/gh-38556/bin/main/com/example/gh38556/Gh38556Application.class]]
Things are now consistent and comma-separated values are being handled. Thanks, @snicoll and @quaff. I'll close this one in favour of the changes that have been made in Framework.