我们使用EnvironmentPostProcessor做了类似的事情,以实现以下命名约定:
- 系统属性
- 环境变量
- "随机"(未使用,但我们保留了默认的PropertySource)
- file:${foo.home}/foo-<profile>.properties
- classpath*:<appName-profile>.properties
- classpath*:application-profile.properties
- classpath*:<appName>.properties
- classpath*:application.properties
- 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;
public class FooPropertiesEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
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() {
}
public static final String APP_KEY = "foo.config.name";
public static void setApplicationName(String appName) {
System.setProperty(APP_KEY, appName);
}
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());
ResourcePatternResolver rpr = resLoader != null ? ResourcePatternUtils.getResourcePatternResolver(resLoader)
: new PathMatchingResourcePatternResolver();
final String suffix = ".properties";
String[] profiles = propRes.getActiveProfiles();
if (profiles.length == 0) {
profiles = new String[] { System.getProperty(DEFAULT_PROFILES_PROPERTY_NAME) };
}
PropertySource<?> removedPropSource = propSources.remove(ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME);
trace(propRes, "removed %s from %s", removedPropSource, propSources);
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(", ")));
}
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) {
return successorName;
}
if (successorName == null) {
propSources.addLast(propSource);
} else if (successorName.equals(propSource.getName())) {
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);
}
}
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 设置任何属性的内容就足够了。