Polymorphic configuration properties in Spring Boot

流过昼夜 提交于 2019-12-04 07:58:47

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:

Self declaring Polymorphic ConfigurationProperties singleton beans

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());
    }
}

Polymorphic ConfigurationProperties beans conditional on properties

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());
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!