diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index e6a72626624..2175a97aced 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -431,6 +431,9 @@ Profile specific properties are loaded from the same locations as standard ones irrespective of whether the profile-specific files are inside or outside your packaged jar. +If several profiles are specified, a last wins strategy applies. For example, profiles +specified by the `spring.active.profiles` property are added after those configured via +the `SpringApplication` API and therefore take precedence. [[boot-features-external-config-placeholders-in-properties]] diff --git a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessor.java index 3006389789f..6d6e1695277 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessor.java @@ -293,23 +293,23 @@ public class ConfigFileEnvironmentPostProcessor implements EnvironmentPostProces this.profiles = Collections.asLifoQueue(new LinkedList()); this.processedProfiles = new LinkedList(); this.activatedProfiles = false; + + Set initialActiveProfiles = null; if (this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) { // Any pre-existing active profiles set via property sources (e.g. System // properties) take precedence over those added in config files. - maybeActivateProfiles( + initialActiveProfiles = maybeActivateProfiles( this.environment.getProperty(ACTIVE_PROFILES_PROPERTY)); } - else { - // Pre-existing active profiles set via Environment.setActiveProfiles() - // are additional profiles and config files are allowed to add more if - // they want to, so don't call addActiveProfiles() here. - List list = new ArrayList( - Arrays.asList(this.environment.getActiveProfiles())); - // Reverse them so the order is the same as from getProfilesForValue() - // (last one wins when properties are eventually resolved) - Collections.reverse(list); - this.profiles.addAll(list); - } + // Pre-existing active profiles set via Environment.setActiveProfiles() + // are additional profiles and config files are allowed to add more if + // they want to, so don't call addActiveProfiles() here. + List list = filterEnvironmentProfiles(initialActiveProfiles != null + ? initialActiveProfiles : Collections.emptySet()); + // Reverse them so the order is the same as from getProfilesForValue() + // (last one wins when properties are eventually resolved) + Collections.reverse(list); + this.profiles.addAll(list); if (this.profiles.isEmpty()) { for (String defaultProfile : this.environment.getDefaultProfiles()) { @@ -415,13 +415,36 @@ public class ConfigFileEnvironmentPostProcessor implements EnvironmentPostProces return propertySource; } - private void maybeActivateProfiles(Object value) { + /** + * Return the active profiles that have not been processed yet. + *

If a profile is enabled via both {@link #ACTIVE_PROFILES_PROPERTY} and + * {@link ConfigurableEnvironment#addActiveProfile(String)} it needs to be + * filtered so that the {@link #ACTIVE_PROFILES_PROPERTY} value takes + * precedence. + *

Concretely, if the "cloud" profile is enabled via the environment, + * it will take less precedence that any profile set via the + * {@link #ACTIVE_PROFILES_PROPERTY}. + * @param initialActiveProfiles the profiles that have been enabled via + * {@link #ACTIVE_PROFILES_PROPERTY} + * @return the additional profiles from the environment to enable + */ + private List filterEnvironmentProfiles(Set initialActiveProfiles) { + List additionalProfiles = new ArrayList(); + for (String profile : this.environment.getActiveProfiles()) { + if (!initialActiveProfiles.contains(profile)) { + additionalProfiles.add(profile); + } + } + return additionalProfiles; + } + + private Set maybeActivateProfiles(Object value) { if (this.activatedProfiles) { if (value != null) { this.logger.debug("Profiles already activated, '" + value + "' will not be applied"); } - return; + return Collections.emptySet(); } Set profiles = getProfilesForValue(value); activateProfiles(profiles); @@ -430,6 +453,7 @@ public class ConfigFileEnvironmentPostProcessor implements EnvironmentPostProces + StringUtils.collectionToCommaDelimitedString(profiles)); this.activatedProfiles = true; } + return profiles; } private void addIncludeProfiles(Object value) { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessorTests.java index 89fac676626..5300afb0c10 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileEnvironmentPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,20 +27,28 @@ import java.util.Collections; import java.util.List; import java.util.Properties; +import ch.qos.logback.classic.BasicConfigurator; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.slf4j.LoggerFactory; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.config.ConfigFileEnvironmentPostProcessor.ConfigurationPropertySources; +import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.env.EnumerableCompositePropertySource; import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.boot.test.OutputCapture; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; @@ -56,12 +64,14 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; /** * Tests for {@link ConfigFileEnvironmentPostProcessor}. @@ -80,8 +90,19 @@ public class ConfigFileEnvironmentPostProcessorTests { @Rule public ExpectedException expected = ExpectedException.none(); + @Rule + public OutputCapture out = new OutputCapture(); + private ConfigurableApplicationContext context; + @Before + public void resetLogging() { + LoggerContext loggerContext = ((Logger) LoggerFactory.getLogger(getClass())) + .getLoggerContext(); + loggerContext.reset(); + BasicConfigurator.configure(loggerContext); + } + @After public void cleanup() { if (this.context != null) { @@ -381,6 +402,67 @@ public class ConfigFileEnvironmentPostProcessorTests { assertThat(property, equalTo("fromprofilepropertiesfile")); } + @Test + public void profilesAddedToEnvironmentAndViaProperty() throws Exception { + // External profile takes precedence over profile added via the environment + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.profiles.active:other"); + this.environment.addActiveProfile("dev"); + this.initializer.postProcessEnvironment(this.environment, this.application); + assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other")); + assertThat(this.environment.getProperty("my.property"), + equalTo("fromotherpropertiesfile")); + validateProfilePrecedence(null, "dev", "other"); + } + + @Test + public void profilesAddedToEnvironmentAndViaPropertyDuplicate() throws Exception { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.profiles.active:dev,other"); + this.environment.addActiveProfile("dev"); + this.initializer.postProcessEnvironment(this.environment, this.application); + assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other")); + assertThat(this.environment.getProperty("my.property"), + equalTo("fromotherpropertiesfile")); + validateProfilePrecedence(null, "dev", "other"); + } + + @Test + public void profilesAddedToEnvironmentAndViaPropertyDuplicateEnvironmentWins() throws Exception { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.profiles.active:other,dev"); + this.environment.addActiveProfile("other"); + this.initializer.postProcessEnvironment(this.environment, this.application); + assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other")); + assertThat(this.environment.getProperty("my.property"), + equalTo("fromdevpropertiesfile")); + validateProfilePrecedence(null, "other", "dev"); + } + + private void validateProfilePrecedence(String... profiles) { + this.initializer.onApplicationEvent(new ApplicationPreparedEvent( + new SpringApplication(), new String[0], new AnnotationConfigApplicationContext())); + String log = this.out.toString(); + + // First make sure that each profile got processed only once + for (String profile : profiles) { + assertThat("Wrong number of occurrences for profile '" + profile + "' --> " + log, + StringUtils.countOccurrencesOf(log, createLogForProfile(profile)), equalTo(1)); + } + // Make sure the order of loading is the right one + for (String profile : profiles) { + String line = createLogForProfile(profile); + int index = log.indexOf(line); + assertTrue("Loading profile '" + profile + "' not found in '" + log + "'", index != -1); + log = log.substring(index + line.length(), log.length()); + } + } + + private String createLogForProfile(String profile) { + String suffix = profile != null ? "-" + profile : ""; + return "Loaded config file 'classpath:/application" + suffix + ".properties'"; + } + @Test public void yamlProfiles() throws Exception { this.initializer.setSearchNames("testprofiles");