Spring boot version: 2.2.7.RELEASE
Consider below test case:
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.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@EnableConfigurationProperties({BindingTest.FooProperties.class, BindingTest.BarProperties.class})
@TestPropertySource(properties = {"foo.bar=baz"})
public class BindingTest {
@Autowired
FooProperties fooProperties;
@Test
public void shouldInjectBar() {
assertThat(fooProperties.barProperties.getBar()).isEqualTo("baz");
}
@ConstructorBinding
@ConfigurationProperties("foo")
public static class FooProperties {
private final BarProperties barProperties;
@Autowired
public FooProperties(BarProperties barProperties) {
this.barProperties = barProperties;
}
public BarProperties getBarProperties() {
return barProperties;
}
}
@ConstructorBinding
@ConfigurationProperties("foo.bar")
public static class BarProperties {
private final String bar;
public BarProperties(String bar) {
this.bar = bar;
}
public String getBar() {
return bar;
}
}
}
Constructor injection of BarProperties
is expected to be done either automatically by spring boot or by explicit use of @Autowired
annotation in the constructor but BarProperties is Null.
If we add @Autowired
to the field then the injection happens and the property is resolved.
@ConstructorBinding
@ConfigurationProperties("foo")
public static class FooProperties {
@Autowired
private final BarProperties barProperties;
public FooProperties(BarProperties barProperties) {
this.barProperties = barProperties;
}
public BarProperties getBarProperties() {
return barProperties;
}
}
Comment From: wilkinsona
Thanks for the sample.
When you nest properties, as you have done with BarProperties
nested beneath FooProperties
, you don't need @ConfigurationProperties
on the nested type. It's the prefix of FooProperties
prefix and the name of the getter that determines the prefix of the properties in BarProperties
. In this case it will be foo.barProperties
and the property to set is foo.barProperties.bar
. You haven't set that property so the behaviour that is described in the documentation becomes relevant:
By default, if no properties are bound to
Security
, theAcmeProperties
instance will contain anull
value forsecurity
. If you wish you return a non-null instance ofSecurity
even when no properties are bound to it, you can use an empty@DefaultValue
annotation to do so.
In your case, FooProperties
is the equivalent of AcmeProperties
and BarProperties
is the equivalent of Security
. Adding @DefaultProperties
to the BarProperties
argument of FooProperties
' constructor will cause an instance of BarProperties
to be injected but bar
will be null
as no value has been bound to it. To bind a value to it, the foo.barProperties.bar
property must be set. Your test will pass if you make the following change to its TestPropertySource
:
@TestPropertySource(properties = {"foo.barProperties.bar=baz"})
Updating your test to align with the structure described in the documentation that I linked to above, it then looks like the following:
import static org.assertj.core.api.Assertions.assertThat;
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.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
@EnableConfigurationProperties(BindingTest.FooProperties.class)
@TestPropertySource(properties = {"foo.bar.baz=baz"})
public class BindingTest {
@Autowired
FooProperties fooProperties;
@Test
public void shouldInjectBar() {
assertThat(fooProperties.bar.getBaz()).isEqualTo("baz");
}
@ConstructorBinding
@ConfigurationProperties("foo")
public static class FooProperties {
private final Bar bar;
@Autowired
public FooProperties(@DefaultValue Bar bar) {
this.bar = bar;
}
public Bar getBarProperties() {
return bar;
}
static class Bar {
private final String baz;
public Bar(String baz) {
this.baz = baz;
}
public String getBaz() {
return baz;
}
}
}
}
If you have any further questions, please follow up on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.
Comment From: HarshadRanganathan
@wilkinsona Probably, I should have added some details around what I'm trying to achieve along with the failing test case. I understand that nested properties would work in the way you have shared in your test case.
My use case is what if I need the nested property to be defined in it's own class file rather than as a static inner class to avoid clutter.
Let's say I have two properties foo.hello=world,foo.bar.baz.bar.baz=baz
which would result in a deeply nested structure for binding property foo.bar.baz.bar.baz
.
I could simply avoid it by defining a class with @ConfigurationProperties("foo.bar.baz.bar") and autowire it in the other class having @ConfigurationProperties("foo").
That's the example I had shown in my test case and the question was why autowiring works at field level but not at a constructor level.
Hope I had clarified the intention.
Comment From: wilkinsona
As noted in the documentation, injecting dependencies into @ConfigurationProperties
classes is discouraged and isn't supported at all with @ConstructorBinding
:
We recommend that
@ConfigurationProperties
only deal with the environment and, in particular, does not inject other beans from the context. For corner cases, setter injection can be used or any of the*Aware
interfaces provided by the framework (such asEnvironmentAware
if you need access to theEnvironment
). If you still want to inject other beans using the constructor, the configuration properties bean must be annotated with@Component
and use JavaBean-based property binding.
As I said above, if you have any further questions please follow up on Stack Overflow or Gitter.