Spring Boot:覆盖用于查找application.properties配置文件的约定

时间:2015-04-27 13:55:59

标签: spring spring-boot

我正在查看位于here

的spring-boot文档

具体是关于考虑财产的顺序的部分:

更具体地说:

打包在jar中的特定于配置文件的应用程序属性(application- {profile} .properties和YAML变体)

首先我要提一下,使用这种方法加载配置文件特定配置时没有任何问题(前提是文件位于类路径中:/或classpath:/ config。

但是,我希望做的是实现如下的约定:

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

此外,我希望在不使用spring.config.location属性的情况下实现此配置。我对Spring Boot很陌生,所以我正在寻找一些提示,告诉我如何实现这个约定。基于我的研究似乎可以通过添加自定义ConfigFileApplicationListener来实现。如果这是一个明智的起点或任何其他可能更好的想法,请告诉我。

更新: 似乎如果我可以以编程方式构建spring.config.location属性列表,我可以在类路径中传递:/ default,classpath:{profile}。基于spring.profiles.active环境变量。以下ConfigFileApplicationListener看起来就像我要调用的那样:

public void setSearchLocations(String locations)

但是,我不确定生命周期中的哪个位置会打电话。

4 个答案:

答案 0 :(得分:3)

所以这就是我设法提出的,不确定我是否会使用此解决方案,但我想我会提供它以防有任何有用的反馈。

所以我尝试在将ConfigFileApplicationListener添加到SpringApplication之后但在触发之前尝试将setSearchLocations(String locations)方法设置为调用。我这样做是通过添加一个也实现Ordered的新侦听器并确保它在ConfigFileApplicationListener之前运行。这似乎做我想要的,但我仍然认为有一个更优雅的方法。我特别不喜欢迭代听众。

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

    }

答案 1 :(得分:0)

我们使用EnvironmentPostProcessor做了类似的事情来实现以下命名约定:

  1. 系统属性
  2. 环境变量
  3. “随机”(未使用,但我们保留了默认的PropertySource)
  4. 文件:$ {foo.home} / foo- &LT;型材&GT; 的.properties
  5. 类路径*:&LT; APPNAME轮廓&GT; 的.properties
  6. 类路径*:application-profile.properties
  7. 类路径*:&LT; APPNAME&GT; 的.properties
  8. 类路径*:application.properties
  9. 类路径*:meta.properties
  10. 某些应用程序没有自己的&lt; appName&gt; ;那些在主类的静态初始化程序中调用setApplicationName来使用这两个附加文件的人。

    这里的hacky部分是我们不排除默认ConfigFileApplicationListener,而是通过删除PropertySource ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME来撤消它。

    File FooPropertiesEnvPostProcessor.java

    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

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

    要在测试中获得相同的属性,他们有@ContextConfiguration(..., initializers = TestAppContextInitializer.class)TestAppContextInitializer实施ApplicationContextInitializer<GenericApplicationContext>并致电 FooPropertiesEnvPostProcessor.addProperties方法中的initialize

    不幸的是,EnvironmentPostProcessor似乎也默认缺少Spring Shell。在我们的情况下(因为只有一小部分 部分应用程序使用Spring Shell),限制<context:component-scan base-package=.../>就足够了 META-INF/spring/spring-shell-plugin.xml中的范围只包含不需要EnvironmentPostProcessor设置的任何属性的东西。

答案 2 :(得分:0)

 # override from outer config , eg. java -jar --spring.profiles.active=your config
 spring.profiles.active=dev 
 spring.config.location=classpath:/${spring.profiles.active}/application.properties

答案 3 :(得分:0)

不需要编写新类的解决方案:

public static void main(String[] args) {
    SpringApplication app = new SpringApplication();
    app.getListeners().stream()
            .filter(listener -> listener instanceof ConfigFileApplicationListener)
            .forEach(configListener -> {
                ((ConfigFileApplicationListener) configListener).setSearchLocations(mySearchLocations);
                ((ConfigFileApplicationListener) configListener).setSearchNames(mySearchNames);
            });
    app.setSources(singleton(MyClassName.class));
    app.run(args);
}