Spring配置文件中的@PropertySources可以被Spring profile所选择吗?

60

我有一个Spring 3.1的@Configuration,需要一个属性foo来构建bean。该属性在defaults.properties中定义,但如果应用程序有一个活动的override Spring配置文件,则可以被overrides.properties中的属性覆盖。

没有覆盖时,代码看起来像这样,并且可以正常工作......

@Configuration
@PropertySource("classpath:defaults.properties")
public class MyConfiguration {

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        ...
        // this.environment.getRequiredProperty("foo");
        ...
    }
}
我想要一个基于@Profile("overrides")@PropertySource,指向classpath:overrides.properties。有没有人有任何想法可以实现这一点?我考虑过复制一个@Configuration,但这样会违反DRY原则;或者编程方式操纵ConfigurableEnvironment,但我不确定environment.getPropertySources.addFirst()应该放在哪里。
如果我使用@Value直接注入属性,则在XML配置文件中放置以下内容有效,但是如果我使用EnvironmentgetRequiredProperty()方法,则无效。
<context:property-placeholder ignore-unresolvable="true" location="classpath:defaults.properties"/>

<beans profile="overrides">
    <context:property-placeholder ignore-unresolvable="true" order="0"
                                  location="classpath:overrides.properties"/>
</beans>

更新

如果您现在正在尝试这样做,请查看Spring Boot的YAML支持,特别是“使用YAML而不是属性”部分。那里的配置文件支持将使这个问题变得无关紧要,但目前还没有@PropertySource支持。

7个回答

57

在静态内部类中添加覆盖@PropertySource。不幸的是,您必须同时指定所有属性源,这意味着创建“默认”配置文件作为“覆盖”的替代方案。

@Configuration
public class MyConfiguration
{
    @Configuration
    @Profile("default")
    @PropertySource("classpath:defaults.properties")
    static class Defaults
    { }

    @Configuration
    @Profile("override")
    @PropertySource({"classpath:defaults.properties", "classpath:overrides.properties"})
    static class Overrides
    {
        // nothing needed here if you are only overriding property values
    }

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        ...
        // this.environment.getRequiredProperty("foo");
        ...
    }
}

10
不理解为什么这个答案得到了这么多赞。硬编码配置文件名称违背了配置文件的初衷。难道没有一种类似的方法可以通过 'spring.profiles.active' 参数来指定配置文件吗? - jjoller
1
今天你可以使用特定于配置文件的属性文件。我不知道在回答这个问题时是否已经可能(请参见此处)。 - Fencer
@Fencer 这仅适用于使用Spring Boot的情况。 - Nima Ajdari
@NimaAJ 这可能是真的,但既然jjoller提到了'spring.profiles.active',我给出的提示可能对那些在他的问题中遇到困难的人很有价值。也许使用'spring.profiles.active'意味着使用Spring Boot或使用特定配置文件的可能性。不过,我应该加上 '@jjoller'。 - Fencer
1
更新:实际上,我更喜欢这种方法而不是Spille和whitebrows的方法,因为如果您有多个活动配置文件,则使用${spring.profiles.active}变量会失败。另一方面,这种@Profile方法可以实现非常灵活的逻辑(在此处记录:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html) - Rhubarb
显示剩余2条评论

24

我建议定义两个文件,第二个文件是可选的,后缀名为profile:

@Configuration
@PropertySources({
        @PropertySource("classpath:/myconfig.properties"),
        @PropertySource(value = "classpath:/myconfig-${spring.profiles.active}.properties", ignoreResourceNotFound = true)
})
public class MyConfigurationFile {

    @Value("${my.prop1}")
    private String prop1;

    @Value("${my.prop2}")
    private String prop2;

}

对我来说,这部分是有魔力的:${spring.profiles.active},现在我可以拥有两个文件:config-dev.propertiesconfig-prod.properties,并且Java类有下一行@PropertySource("classpath:config-${spring.profiles.active}.properties") - Jairo Martínez
10
如果您有多个配置文件,则此方法将失败。(例如,我有活动配置文件=“dev,mock”) - Rhubarb
我很幸运能找到这个答案。简单明了,你救了我。 - Vodka
1
你可以使用 ${spring.active.profiles} 来解决多个配置文件的问题。 - Joel Eze
${spring.active.profiles}不存在,而且我也不知道它怎么解决多个配置文件的问题。 - Ben M

10

你可以做:

  <context:property-placeholder location="classpath:${spring.profiles.active}.properties" />

编辑:如果你需要更高级的功能,你可以在应用启动时注册你的PropertySources。

web.xml

  <context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>com.xxx.core.spring.properties.PropertySourcesApplicationContextInitializer</param-value>
  </context-param>

您创建的文件:

public class PropertySourcesApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  private static final Logger LOGGER = LoggerFactory.getLogger(PropertySourcesApplicationContextInitializer.class);

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    LOGGER.info("Adding some additional property sources");
    String[] profiles = applicationContext.getEnvironment().getActiveProfiles()
    // ... Add property sources according to selected spring profile 
    // (note there already are some property sources registered, system properties etc)
    applicationContext.getEnvironment().getPropertySources().addLast(myPropertySource);
  }

}

完成之后,您只需要添加您的上下文:

<context:property-placeholder/>

关于多个配置文件的问题,我无法给出确切的答案,但我猜测你可以通过一个初始化器来激活它们,并且在配置文件激活期间注册相应的PropertySource项。


2
使用applicationContext.getEnvironment().getActiveProfiles(),您可以访问已解析的活动配置文件列表作为一个数组。 - David Harkness
没错,你可以两者兼顾。当时我不知道我们可以传递多个配置文件 ;) - Sebastien Lorber

5

我想不到其他方法,只能按照你提出的建议,使用一个单独的@Configuration文件来定义这个bean,并加上一个@Profile注解:

@Configuration
@Profile("override")
@PropertySource("classpath:override.properties")
public class OverriddenConfig {

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        //if..
    }
}

5

如果您需要支持多个配置文件,则可以采取以下措施:

@Configuration
public class Config {

    @Configuration
    @Profile("default")
    @PropertySource("classpath:application.properties")
    static class DefaultProperties {
    }

    @Configuration
    @Profile("!default")
    @PropertySource({"classpath:application.properties", "classpath:application-${spring.profiles.active}.properties"})
    static class NonDefaultProperties {
    }
}

这样你就不需要为每个配置文件定义一个静态配置类了。 感谢David Harkness指导我正确的方向。


失败原因是存在多个活动配置文件 - 最好还是创建单独的@Profile设置。 - Rhubarb

3

注意:本答案提供了一种使用@PropertySource属性文件的替代方案。我选择这条路是因为尝试处理可能每个都具有覆盖的多个属性文件,同时避免重复代码过于繁琐。

为每个相关的属性集创建一个POJO接口以定义它们的名称和类型。

public interface DataSourceProperties
{
    String driverClassName();
    String url();
    String user();
    String password();
}

实现返回默认值。

public class DefaultDataSourceProperties implements DataSourceProperties
{
     public String driverClassName() { return "com.mysql.jdbc.Driver"; }
     ...
}

为每个配置文件创建子类(例如:开发,生产),并覆盖与默认值不同的任何值。这需要一组互斥的配置文件,但您可以轻松地将“默认”作为“覆盖”选项的替代品。

@Profile("production")
@Configuration
public class ProductionDataSourceProperties extends DefaultDataSourceProperties
{
     // nothing to override as defaults are for production
}

@Profile("development")
@Configuration
public class DevelopmentDataSourceProperties extends DefaultDataSourceProperties
{
     public String user() { return "dev"; }
     public String password() { return "dev"; }
}

最后,将属性配置自动装配到需要它们的其他配置中。这里的优点是你不需要重复任何@Bean创建代码。

@Configuration
public class DataSourceConfig
{
    @Autowired
    private DataSourceProperties properties;

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource source = new BoneCPDataSource();
        source.setJdbcUrl(properties.url());
        ...
        return source;
    }
}

我还不确定是否会使用这种方法来代替基于servlet上下文初始化器中活动配置文件的手动配置属性文件。我的想法是手动配置可能不太适合单元测试,但现在我不确定了。我更喜欢读取属性文件而不是一系列属性访问器列表。


1
关于属性和@PropertySource的主要特点是,您可以以各种方式覆盖它 - 例如使用环境变量或应用程序开关。 - Raniz

1

所有提到的解决方案都有些不太方便,只能使用一个配置文件预设,并且它们无法与更多/其他配置文件一起使用。目前,Spring团队拒绝引入此功能。但是我找到了一个有效的解决方法:

package com.example;

public class MyPropertySourceFactory implements PropertySourceFactory, SpringApplicationRunListener {

    public static final Logger logger = LoggerFactory.getLogger(MyPropertySourceFactory.class);

    @NonNull private static String[] activeProfiles = new String[0];

    // this constructor is used for PropertySourceFactory
    public MyPropertySourceFactory() {
    }

    // this constructor is used for SpringApplicationRunListener
    public MyPropertySourceFactory(SpringApplication app, String[] params) {
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        activeProfiles = environment.getActiveProfiles();
    }

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
        logger.info("Loading: {} with profiles: {}", encodedResource.toString(), activeProfiles);
        // here you know all profiles and have the source Resource with main
        // properties, just try to load other resoures in the same path with different 
        // profile names and return them as a CompositePropertySource
    }
}

为了使其工作,您需要拥有以下内容的src/main/resources/META-INF/spring.factories
org.springframework.boot.SpringApplicationRunListener=com.example.MyPropertySourceFactory

现在您可以将自定义属性文件放在任何位置,并使用@PropertySources加载它:
@Configuration
@PropertySource(value = "classpath:lib.yml", factory = MyPropertySourceFactory.class)
public class PropertyLoader {
}

我不会说“拒绝”。他们表达了自己的立场并且很有道理……但是,这仍然是一个有帮助的解决方法。 - Dominik21

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