I would like to use polymorphic configuration properties on Spring, using Spring's @ConfigurationProperties
annotation.
Suppose we have the following POJO classes.
public class Base {
private String sharedProperty;
public String getSharedProperty() {
return sharedProperty;
}
public String setSharedProperty(String sharedProperty) {
this.sharedProperty = sharedProperty;
}
}
public class Foo extends Base {
private String fooProperty;
public String getFooProperty() {
return fooProperty;
}
public String setFooProperty(String sharedProperty) {
this. fooProperty = fooProperty;
}
}
public class Bar extends Base {
private String barProperty;
public String getSharedProperty() {
return sharedProperty;
}
public String setBarProperty(String barProperty) {
this.barProperty = barProperty;
}
}
And the configuration properties class,
@Component
@ConfigurationProperties(prefix = "playground")
public class SomeConfigurationProperties {
private List<Base> mixed;
public List<Base> getMixed() {
return mixed;
}
public void setMixed(List<Base> mixed) {
this.mixed = mixed;
}
}
And the application.yml
file,
playground:
mixed:
- shared-property: "shared prop"
foo-property: "foo prop"
- shared-property: "shared prop"
bar-property: "bar prop"
However, with this configuration, Spring initializes the @ConfigurationProperties
-annotated class with the list of Base
objects, instead of their subclasses. That is, actually, an expected behavior (due to security concerns).
Is there a way to enforce the behavior of SnakeYAML to use subclasses, or implement any kind of custom deserialization provider?
Although it is possible to implement custom PropertySources and/or ConversionService, a custom deserialization provider is not necessary.
Spring has no issues binding the same properties to multiple beans. The reason your implementation is not working is because you are only registering one bean with the ApplicationContext with the @Component
annotation on the base class. This is telling the component scanner that there is only one singleton of type Base
. Because Foo
and Bar
are not registered as beans, they won't be bound to.
If the only reason you are looking at making these polymorphic is to share property name prefixes in SnakeYAML based config, then you actually do not need to introduce the polymorphic relationship, and can bind to shared properties by a common field name in different classes.
There are many ways to implement what you are asking for however in a polymorphic way, here are a few of the most straight forward simple ones:
Instead of applying the @ConfigurationProperties
and @Component
annotations on the base class, apply them on the concrete classes, with the same property name prefix. This wouldn't be my preferred approach, as each bean would not be conditional on their properties being set, however it may suit your needs. Depending on if your Spring Configuration allows properties to be reloaded, Spring will maintain the bindings on all of the beans.
Note: As of IntelliJ Idea 2018.3, an inspection profile was added to identify duplicate prefix keys as an error. You may want to ignore this, or suppress the warnings.
I tested the following successfully:
Base.java
package sample;
public class Base {
private String sharedProperty;
public String getSharedProperty() {
return sharedProperty;
}
public void setSharedProperty(String sharedProperty) {
this.sharedProperty = sharedProperty;
}
}
Foo.java
package sample;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("playground")
public class Foo extends Base {
private String fooProperty;
public String getFooProperty() {
return fooProperty;
}
public void setFooProperty(String fooProperty) {
this.fooProperty = fooProperty;
}
}
Bar.java
package sample;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("playground")
public class Bar extends Base {
private String barProperty;
public String getBarProperty() {
return barProperty;
}
public void setBarProperty(String barProperty) {
this.barProperty = barProperty;
}
}
application.yml
playground:
shared-property: "shared prop"
foo-property: "foo prop"
bar-property: "bar prop"
SampleAppTest.java
package sample;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
public class SampleAppTest {
@Autowired
public Environment environment;
@Test
public void test(@Autowired Bar bar, @Autowired Foo foo) {
assertEquals("shared prop", bar.getSharedProperty());
assertEquals("shared prop", foo.getSharedProperty());
assertEquals("bar prop", bar.getBarProperty());
assertEquals("foo prop", foo.getFooProperty());
}
@Test
public void testSuper(@Autowired List<Base> props) {
assertEquals(2, props.size());
}
}
You may not want certain concrete implementations to be instantiated if their specific properties are missing. Furthermore, you may not want to couple the @ConfigurationProperties
and @Component
annotations to each concrete class. This implementation constructs the ConfigurationProperties beans via a Spring @Configuration
bean. The configuration bean ensures they are only constructed conditionally via a property existence check. This implementation also creates a bean of concrete type Base
if none of the other Base
beans meet conditions and the shared properties exist. The same unit test from the previous example is used here and passes:
Base.java
package sample;
public class Base {
private String sharedProperty;
public String getSharedProperty() {
return sharedProperty;
}
public void setSharedProperty(String sharedProperty) {
this.sharedProperty = sharedProperty;
}
}
Foo.java
package sample;
public class Foo extends Base {
private String fooProperty;
public String getFooProperty() {
return fooProperty;
}
public void setFooProperty(String fooProperty) {
this.fooProperty = fooProperty;
}
}
Bar.java
package sample;
public class Bar extends Base {
private String barProperty;
public String getBarProperty() {
return barProperty;
}
public void setBarProperty(String barProperty) {
this.barProperty = barProperty;
}
}
SampleConfiguration.java
package sample;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SampleConfiguration {
@Bean
@ConfigurationProperties("playground")
@ConditionalOnProperty("playground.foo-property")
public Foo foo() {
return new Foo();
}
@Bean
@ConfigurationProperties("playground")
@ConditionalOnProperty("playground.bar-property")
public Bar bar() {
return new Bar();
}
@Bean
@ConfigurationProperties("playground")
@ConditionalOnProperty("playground.shared-property")
@ConditionalOnMissingBean(Base.class)
public Base base() {
return new Base();
}
}
application.yml
playground:
shared-property: "shared prop"
foo-property: "foo prop"
bar-property: "bar prop"
SampleAppTest.java
package sample;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
public class SampleAppTest {
@Autowired
public Environment environment;
@Test
public void test(@Autowired Bar bar, @Autowired Foo foo) {
assertEquals("shared prop", bar.getSharedProperty());
assertEquals("shared prop", foo.getSharedProperty());
assertEquals("bar prop", bar.getBarProperty());
assertEquals("foo prop", foo.getFooProperty());
}
@Test
public void testSuper(@Autowired List<Base> props) {
assertEquals(2, props.size());
}
}