Fabio Maffioletti About Curriculum Blog


Another approach for handling deprecation of the locations property of Spring Boot @ConfigurationProperties

So the time has come: with Spring Boot 1.5 the locations property of the @ConfigurationProperties annotation has been removed. It's time to deal with it so I tried to integrate the proposal made at the end of december in existing projects. It worked quite well for most of them, but it could be improved.

A simpler approach

I tried to simplify the approach, without having the need for an application context and without having to scan the classpath to get all the beans annotated with @ApplicationProperties. In fact I removed the annotation and the class described in the mentioned article, and I took advantage of the Spring's @Configuration annotation. With this approach you just need to declare a class like this:

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.boot.env.PropertySourcesLoader;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.PropertySources;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.lang.reflect.Constructor;

@Configuration
public class ApplicationConfiguration implements ResourceLoaderAware {

    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    // here go the configuration beans declarations... see the rest of the article for instructions

    private <T> T bindPropertiesToTarget(Class<T> clazz, String prefix, String... locations) {
        try {
            Constructor<T> constructor = clazz.getConstructor();
            T newInstance = constructor.newInstance();

            PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<>(newInstance);
            factory.setPropertySources(loadPropertySources(locations));
            factory.setConversionService(new DefaultConversionService());
            if (StringUtils.isNotBlank(prefix)) {
                factory.setTargetName(prefix);
            }
            factory.bindPropertiesToTarget();
            return newInstance;

        } catch (Exception ex) {
            String targetClass = ClassUtils.getShortName(clazz);
            throw new BeanCreationException(clazz.getSimpleName(), "Could not bind properties to " + targetClass + " (" + clazz.getSimpleName() + ")", ex);
        }
    }

    private PropertySources loadPropertySources(String[] locations) {
        try {
            PropertySourcesLoader loader = new PropertySourcesLoader();
            for (String location : locations) {
                Resource resource = this.resourceLoader.getResource(location);
                loader.load(resource);
            }
            return loader.getPropertySources();
        }
        catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

So now you have a configuration class that is going to be picked up by Spring to actually build the application configuration. Now, it's time to declare the beans to make the configuration pojos injectable into application's beans, and it can be done just by declaring them as usual @Beans. For example, given this pojo:

import lombok.Data;

/**
 * Created by fmaffioletti on 2/9/17.
 */
@Data
public class MyConfiguration {

    private String key;

}

It can be bound to a Spring bean like this (replace the comment I made in the ApplicationConfiguration class):

@Bean
public MyConfiguration myConfiguration() {
    return bindPropertiesToTarget(MyConfiguration.class, null, "classpath:my-configuration.yml");
}

That's it! So the final class would look like the following. You can keep adding condiguration beans in this class, so you'll keep everything clean and manageable.

@Configuration
public class ApplicationConfiguration implements ResourceLoaderAware {

    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    @Bean
    public MyConfiguration myConfiguration() {
        return bindPropertiesToTarget(MyConfiguration.class, null, "classpath:my-configuration.yml");
    }

    private <T> T bindPropertiesToTarget(Class<T> clazz, String prefix, String... locations) {
        try {
            Constructor<T> constructor = clazz.getConstructor();
            T newInstance = constructor.newInstance();

            PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<>(newInstance);
            factory.setPropertySources(loadPropertySources(locations));
            factory.setConversionService(new DefaultConversionService());
            if (StringUtils.isNotBlank(prefix)) {
                factory.setTargetName(prefix);
            }
            factory.bindPropertiesToTarget();
            return newInstance;

        } catch (Exception ex) {
            String targetClass = ClassUtils.getShortName(clazz);
            throw new BeanCreationException(clazz.getSimpleName(), "Could not bind properties to " + targetClass + " (" + clazz.getSimpleName() + ")", ex);
        }
    }

    private PropertySources loadPropertySources(String[] locations) {
        try {
            PropertySourcesLoader loader = new PropertySourcesLoader();
            for (String location : locations) {
                Resource resource = this.resourceLoader.getResource(location);
                loader.load(resource);
            }
            return loader.getPropertySources();
        }
        catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

Resources

You can find the example project here along with tests and example files.

comments powered by Disqus