Spring Boot: Override convention used to locate .properties application config files

I have looked at the spring-boot documentation located here

Specifically, the section on ordering in which properties are discussed:

More specific:

Profile specific application properties wrapped inside your jar (application- {profile} .properties and YAML variants)

Let me first mention that I have no problem loading profile configurations using this approach (assuming the files are on classpath: / or classpath: / config.

However, what I hope to do is implement a convention similar to the following:

classpath:/default/application.properties
classpath:/{profile}/application.properties

      

Also, I would like to achieve this configuration without using a property spring.config.location

. I'm new to Spring Boot, so I'm looking for some hints on how I will enforce this convention. Based on my research It seems like this can be achieved by adding a custom ConfigFileApplicationListener. Please let me know if this is a reasonable starting point or any other ideas that might be better.

Update: It looks like if I could programmatically build a spring.config.location

list of properties that I could pass in places like classpath: / default, classpath: {profile}. based on spring.profiles.active variable. The following ConfigFileApplicationListener is similar to the one I want to call:

public void setSearchLocations(String locations)

      

However, I'm not sure where in the lifecycle I would make such a call.

+3


source to share


2 answers


So here's what I managed to come up with, not sure if I would even go with this solution, but I figured I would suggest it in case there is any useful feedback.

So I resorted to trying to set the setSearchLocations (String places) method to ConfigFileApplicationListener

after it was added to SpringApplication but before it was run. I did this by adding a new listener that also implements Ordered and made sure it worked before ConfigFileApplicationListener

. This is similar to what I want, but I still think there is a more elegant approach. I don't particularly like to iterate over listeners.



public class LocationsSettingConfigFileApplicationListener implements
        ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

    /**
     * this should run before ConfigFileApplicationListener so it can set its
     * state accordingly
     */
    @Override
    public int getOrder() {
        return ConfigFileApplicationListener.DEFAULT_ORDER - 1;
    }

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {

        SpringApplication app = event.getSpringApplication();
        ConfigurableEnvironment env = event.getEnvironment();

        for (ApplicationListener<?> listener : app.getListeners()) {

            if (listener instanceof ConfigFileApplicationListener) {
                ConfigFileApplicationListener cfal = (ConfigFileApplicationListener) listener;
                //getSearchLocations omitted
                cfal.setSearchLocations(getSearchLocations(env));
            }
        }

    }

      

+2


source


We did something similar with EnvironmentPostProcessor to achieve the following naming convention:

  • System properties
  • Environment Variables
  • "random" (not used, but we kept this PropertySource by default)
  • file: $ {} foo.home / foo- <profile> .properties
  • * classes: <APPNAME-profile> .properties
  • CLASSPATH *: application-profile.properties
  • * classes: <APPNAME> .properties
  • CLASSPATH *: application.properties
  • CLASSPATH *: meta.properties

Some applications do not have their own <appName>; the ones that are called setApplicationName

in the static initializer of the main class to use these two additional files.

The hacky part here is that we don't exclude by default ConfigFileApplicationListener

, but will reverse it by removing the PropertySource ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME

.

FooPropertiesEnvPostProcessor.java file



package example.foo.utils.spring;

import static org.springframework.core.env.AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME;
import java.io.IOException;
import java.util.List;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.PropertySourcesLoader;
import org.springframework.boot.logging.LoggingApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;

/**
 * Configures environment properties according to the FOO conventions.
 */
public class FooPropertiesEnvPostProcessor implements EnvironmentPostProcessor, Ordered {

    /**
     * Order before LoggingApplicationListener and before
     * AutowiredAnnotationBeanPostProcessor. The position relative to
     * ConfigFileApplicationListener (which we want to override) should not
     * matter: If it runs before this, we remove its PropertySource; otherwise,
     * its PropertySource remains but should do no harm as it is added at the
     * end.
     */
    public static final int ORDER
        = Math.min(LoggingApplicationListener.DEFAULT_ORDER, new AutowiredAnnotationBeanPostProcessor().getOrder()) - 1;

    static {
        System.setProperty(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME,
                System.getProperty(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME, "production"));
    }

    public FooPropertiesEnvPostProcessor() {
    }

    /**
     * Property key used as the application (sub-project) specific part in
     * properties file names.
     * <p>
     * <strong>Note:</strong> Direct access to this property key is meant for
     * tests which set the property in an annotation (e.g.
     * {@link IntegrationTest}). However, SpringBootApplications which need to
     * set this system property before Spring initialization should call
     * {@link #setApplicationName(String) setApplicationName} instead.
     * </p>
     */
    public static final String APP_KEY = "foo.config.name";

    /**
     * Sets the application name used to find property files (using
     * {@link FooPropertiesEnvPostProcessor}).
     *
     * @param appName
     *            the application name
     */
    public static void setApplicationName(String appName) {
        System.setProperty(APP_KEY, appName);
    }

    /**
     * Replacement for logging, which is not yet initialized during
     * postProcessEnvironment.
     */
    static void log(String format, Object... args) {
        System.out.println(String.format(format, args));
    }

    static void debug(PropertyResolver env, String format, Object... args) {
        String level = env.getProperty("logging.level." + FooPropertiesEnvPostProcessor.class.getName());
        if ("trace".equalsIgnoreCase(level) || "debug".equalsIgnoreCase(level)) {
            log(format, args);
        }
    }

    static void trace(PropertyResolver env, String format, Object... args) {
        String level = env.getProperty("logging.level." + FooPropertiesEnvPostProcessor.class.getName());
        if ("trace".equalsIgnoreCase(level)) {
            log(format, args);
        }
    }

    @Override
    public int getOrder() {
        return ORDER;
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        addProperties(environment.getPropertySources(), application.getResourceLoader(), environment);
    }

    public static void addProperties(MutablePropertySources propSources, ResourceLoader resLoader, ConfigurableEnvironment propRes) {
        trace(propRes, "FooPropertiesEnvPostProcessor.addProperties(..)");
        List<PropertySourceLoader> psls = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                PropertySourcesLoader.class.getClassLoader());
        // ResourcePatternUtils does not accept null yet
        // (https://jira.spring.io/browse/SPR-14500)
        ResourcePatternResolver rpr = resLoader != null ? ResourcePatternUtils.getResourcePatternResolver(resLoader)
                : new PathMatchingResourcePatternResolver();
        final String suffix = ".properties"; // SonarQube made me declare this
        String[] profiles = propRes.getActiveProfiles();
        if (profiles.length == 0) {
            profiles = new String[] { System.getProperty(DEFAULT_PROFILES_PROPERTY_NAME) };
        }

        // ConfigFileApplicationListener adds PropertySource "applicationConfigurationProperties" consisting of
        // - "applicationConfig: [classpath:/${spring.config.name}-<profile>.properties]"
        // - "applicationConfig: [classpath:/${spring.config.name}.properties]"
        // Since we want the profile to have higher priority than the app name, we cannot just set
        // "spring.config.name" to the app name, use ConfigFileApplicationListener, and add
        // "application-<profile>.properties" and "application.properties".
        // Instead, remove ConfigFileApplicationListener:
        PropertySource<?> removedPropSource = propSources.remove(ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME);
        trace(propRes, "removed %s from %s", removedPropSource, propSources);

        // add meta.properties at last position, then others before the previously added. => resulting order:
        // - { systemProperties
        // - systemEnvironment
        // - random } - already added automatically elsewhere
        // - file:${foo.home}/foo-<profile>.properties
        // - classpath:<appName>-<profile>.properties
        // - classpath:application-<profile>.properties
        // - classpath:<appName>.properties
        // - classpath:application.properties
        // - classpath:meta.properties
        // By adding ${foo.home}/... (chronlogically) last, the property can be set in the previously added resources.
        boolean defaultAppName = "application".equals(propRes.resolveRequiredPlaceholders("${" + APP_KEY + ":application}"));
        String psn = null;
        psn = addProperties(propSources, propRes, rpr, psls, true, psn, propRes.resolveRequiredPlaceholders("classpath*:meta" + suffix));
        psn = addProperties(propSources, propRes, rpr, psls, true, psn, propRes.resolveRequiredPlaceholders("classpath*:application" + suffix));
        if (!defaultAppName) {
            psn = addProperties(propSources, propRes, rpr, psls, false,
                    psn, propRes.resolveRequiredPlaceholders("classpath*:${" + APP_KEY + ":application}" + suffix));
        }
        for (String profile : profiles) {
            psn = addProperties(propSources, propRes, rpr, psls, false, psn,
                    propRes.resolveRequiredPlaceholders("classpath*:application-" + profile + suffix));
        }
        if (!defaultAppName) {
            for (String profile : profiles) {
                psn = addProperties(propSources, propRes, rpr, psls, false,
                        psn, propRes.resolveRequiredPlaceholders("classpath*:${" + APP_KEY + ":application}-" + profile + suffix));
            }
        }
        for (String profile : profiles) {
            psn = addProperties(propSources, propRes, rpr, psls, false,
                    psn, propRes.resolveRequiredPlaceholders("file:${foo.home:.}/foo-" + profile + suffix));
        }

        Stream<PropertySource<?>> propSourcesStream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(propSources.iterator(), 0), false);
        debug(propRes, "Property sources: %s%n", propSourcesStream.map(PropertySource::getName).collect(Collectors.joining(", ")));
    }

    /**
     * Adds a resource given by location string to the given PropertySources, if
     * it exists.
     *
     * @param propSources
     *            the property sources to modify
     * @param successorName
     *            the name of the (already added) successor resource, i.e. the
     *            resource before which the new one should be added; if null,
     *            add as last resource
     * @param location
     *            the location of the resource to add
     * @return the name of the newly added resource, or {@code successorName} if
     *         not added
     */
    private static String addProperties(MutablePropertySources propSources, PropertyResolver propRes, ResourcePatternResolver resLoader,
            List<PropertySourceLoader> propLoaders, boolean required, String successorName, String location) {
        Resource[] resources;
        try {
            resources = resLoader.getResources(location);
        } catch (IOException e) {
            throw new IllegalStateException("failed to load property source " + location + ": " + e, e);
        }
        if (resources.length == 0) {
            debug(propRes, "%s property resource not found: %s", required ? "required" : "optional", location);
            if (required) {
                throw new IllegalStateException("required property source " + location + " not found");
            } else {
                return successorName;
            }
        }

        String newSuccessorName = successorName;
        for (Resource resource : resources) {
            boolean exists = resource.exists();
            debug(propRes, "%s property resource %sfound: %s%s", required ? "required" : "optional", exists ? "" : "not ", location,
                    uriDescription(resource, propRes));
            if (!required && !exists) {
                continue;
            }

            boolean loaded = false;
            for (PropertySourceLoader propLoader : propLoaders) {
                if (canLoadFileExtension(propLoader, resource)) {
                    newSuccessorName = addResource(propSources, propRes, resource, propLoader, newSuccessorName);
                    loaded = true;
                    break;
                }
            }
            if (!loaded && required) {
                throw new IllegalStateException("No PropertySourceLoader found to load " + resource);
            }
        }
        return newSuccessorName;
    }

    private static String addResource(MutablePropertySources propSources, PropertyResolver propRes, Resource resource,
            PropertySourceLoader propLoader, String successorName) {
        try {
            PropertySource<?> propSource = propLoader.load(resource.getDescription(), resource, null);
            if (propSource == null) {
                // e.g. a properties file with everything commented;
                // org.springframework.boot.env.PropertiesPropertySourceLoader
                // converts empty to null
                return successorName;
            }
            if (successorName == null) {
                propSources.addLast(propSource);
            } else if (successorName.equals(propSource.getName())) {
                // happens if APP_KEY is not set, so that
                // "${APP_KEY:application}" == "application"
                trace(propRes, "skipping duplicate resource %s", successorName);
            } else {
                propSources.addBefore(successorName, propSource);
            }
            return propSource.getName();
        } catch (IOException e) {
            throw new IllegalStateException("Unable to load configuration file " + resource + ": " + e, e);
        }
    }

    /**
     * Stolen from {@link PropertySourcesLoader}
     */
    private static boolean canLoadFileExtension(PropertySourceLoader loader, Resource resource) {
        String filename = resource.getFilename().toLowerCase();
        for (String extension : loader.getFileExtensions()) {
            if (filename.endsWith("." + extension.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static String uriDescription(Resource resource, PropertyResolver propRes) {
        try {
            return resource.exists() ? (" in " + resource.getURI()) : "";
        } catch (IOException e) {
            trace(propRes, "getURI: %s", e);
            return "";
        }
    }
}

      

META-INF / spring.factories file

org.springframework.boot.env.EnvironmentPostProcessor = example.foo.utils.spring.FooPropertiesEnvPostProcessor

      

To get the same properties in tests, they have @ContextConfiguration(..., initializers = TestAppContextInitializer.class)

. TestAppContextInitializer

implements ApplicationContextInitializer<GenericApplicationContext>

and calls FooPropertiesEnvPostProcessor.addProperties

in a method initialize

.

Unfortunately, it looks like there is no Spring Shell for EnvironmentPostProcessor by default. In our case (since only a tiny part of the application uses the Spring Shell), it was sufficient to restrict the <context:component-scan base-package=.../>

scope META-INF/spring/spring-shell-plugin.xml

to only contain materials that do not need any of the properties set by the EnvironmentPostProcessor.

0


source







All Articles