Jonathan Martin Jonathan Martin - 3 months ago 42
Java Question

How to build custom PropertySource for binding to @ConfigurationProperties

We're creating a new

PropertySource
that uses a database as it's repository. The idea is that we can update property values at runtime.

At the same time, we'd like to use
@ConfigurationProperties
to so we can include validation as well as use the application-{profile} naming conventions.

However, it appears that values of
@ConfigurationProperties
are only loaded from "applicationConfig: [path/to/config]"
PropertySource
's. For example, the following test:

private final String OVERRIDEN_VALUE = "overriden value";

@Before
public void before() {

LOGGER.debug("Property sources are: ");
for(Iterator<?> it = env.getPropertySources().iterator(); it.hasNext(); ) {
PropertySource<?> propertySource = (PropertySource<?>) it.next();
LOGGER.debug(propertySource.getName());
}

EnvironmentTestUtils.addEnvironment("integrationTest", env, "some.prefix.overridable-property=" + OVERRIDEN_VALUE);

}

@Test
public void testOverridingDefaultProperties() {

LOGGER.debug("MutablePropertySources value: {}", env.getProperty("some.prefix.overridable-property"));
LOGGER.debug("@ConfigurationProperties value: {}", testProperties.getOverridableProperty());

Assert.assertEquals(OVERRIDEN_VALUE, testProperties.getOverridableProperty());

}


Produces this output:

Property sources are:
systemProperties
systemEnvironment
random
integrationTest
applicationConfig: [classpath:/path/to/my/application.yml]

MutablePropertySources value: overriden value
@ConfigurationProperties value: default value


For more context, I originally asked this question on Spring Boot's Github here.

Answer

Thanks to the Spring Boot folks. Pointed me to Spring Cloud Context

http://projects.spring.io/spring-cloud/spring-cloud.html#customizing-bootstrap-property-sources

Looks like this will do the trick.

Update:

Since we already had the additional database-backed PropertySource written we really only needed to refresh the @ConfigurationProperties at runtime. At the same time we didn't want to refresh all @ConfigurationProperties. To accomplish the refresh we did the following:

  1. Created an annotation called @ReloadableProperties
  2. Created the following utility bean which utilizes Spring Boot's ConfigurationPropertiesBindingPostProcessor
/**
 * 
 * Helper bean to reload {@code @ConfigurationProperties}
 * if the {@code @ConfigurationProperties} bean is annotated 
 * with {@code @ReloadableProperties}.
 * 
 * @author Jonathan Martin
 * @since 2.0.0
 * 
 * @see ReloadableProperties
 * @see ConfigurationPropertiesBindingPostProcessor
 *
 */
public class ConfigurationPropertiesReloader {

    private final ApplicationContext context;

    private final ConfigurationPropertiesBindingPostProcessor processor;

    @Autowired
    public ConfigurationPropertiesReloader(ApplicationContext context,  ConfigurationPropertiesBindingPostProcessor processor) {
        this.context = context;
        this.processor = processor;
    }

    /**
     * Reload all {@code @ConfigurationProperties}
     * annotated with {@code @ReloadableProperties}.
     */
    public void reload() {
        Map beans = context.getBeansWithAnnotation(ReloadableProperties.class);
        for (Map.Entry entry : beans.entrySet()) {

            String beanName = entry.getKey();
            Object bean = entry.getValue();

            ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);

            // Only reload the bean if it's an @ConfigurationProperties
            // Can't check for instance of ConfigurationPropertiesHolder 
            // because it uses package scope.
            if (annotation != null) {
                processor.postProcessBeforeInitialization(bean, beanName);
            }

        }
    }

}

Now if I inject the utility bean into my test and invoke reload, the test passes.