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

4

我在查看位于此处的spring-boot文档。

具体来说,是关于考虑属性顺序的部分:

更具体地说:

打包在 JAR 内的特定配置文件(application-{profile}.properties 和 YAML 变种)

首先,我要提到的是,如果这些文件位于classpath:/或classpath:/config中,使用此方法加载配置文件时,我没有遇到任何问题。

然而,我希望实现以下约定:

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

此外,我希望在不使用spring.config.location属性的情况下实现此配置。我对Spring Boot相当新,因此我正在寻找一些提示,了解如何实现此约定。 根据我的研究,似乎可以通过添加自定义ConfigFileApplicationListener来实现这一点。请告诉我是否这是一个明智的起点或者是否有更好的想法。
更新: 看起来,如果我可以以编程方式构建出spring.config.location属性列表,我可以传递诸如classpath:/default,classpath:{profile}等位置,根据spring.profiles.active环境变量。以下ConfigFileApplicationListener似乎是我想要调用的:
public void setSearchLocations(String locations)

然而,我不确定在生命周期的哪个阶段进行这样的调用。


1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Kakawait
4个回答

5
这里是我想到的解决方法,不确定是否会采用,但我觉得还是提供一下,以便获得有用的反馈。
我试图在将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
一种不需要编写新类的解决方案:
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);
}

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

回答问题时,请回答提问者所问的问题。如果发布代码,请用几句话解释代码的作用。 - Jan B.

0

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

  1. 系统属性
  2. 环境变量
  3. "随机"(未使用,但我们保留了默认的PropertySource)
  4. file:${foo.home}/foo-<profile>.properties
  5. classpath*:<appName-profile>.properties
  6. classpath*:application-profile.properties
  7. classpath*:<appName>.properties
  8. classpath*:application.properties
  9. classpath*:meta.properties

有些应用程序没有自己的<appName>;那些有的则在主类的静态初始化器中调用setApplicationName来使用这两个附加文件。

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

文件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>并在其initialize方法中调用FooPropertiesEnvPostProcessor.addProperties
不幸的是,默认情况下,EnvironmentPostProcessor 似乎也缺少 Spring Shell。在我们的情况下(因为应用程序的一小部分仅使用 Spring Shell),将 <context:component-scan base-package=.../> 范围限制在META-INF/spring/spring-shell-plugin.xml中仅包含不需要由 EnvironmentPostProcessor 设置任何属性的内容就足够了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接