Skip to content

Commit 7c1bf58

Browse files
committed
Filter duplicate
Improve the initial PR to include a filtering of the profiles that were already enabled via the `spring.profiles.active` property. Also add more tests to prove that each profile is loaded only once now. Closes gh-4273
1 parent 06bb6bd commit 7c1bf58

File tree

3 files changed

+109
-16
lines changed

3 files changed

+109
-16
lines changed

spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ Profile specific properties are loaded from the same locations as standard
369369
ones irrespective of whether the profile-specific files are inside or outside your
370370
packaged jar.
371371

372+
If several profiles are specified, a last wins strategy applies. For example, profiles
373+
specified by the `spring.active.profiles` property are added after those configured via
374+
the `SpringApplication` API and therefore take precedence.
372375

373376

374377
[[boot-features-external-config-placeholders-in-properties]]

spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
*
9090
* @author Dave Syer
9191
* @author Phillip Webb
92+
* @author Stephane Nicoll
9293
*/
9394
public class ConfigFileApplicationListener
9495
implements ApplicationListener<ApplicationEvent>, Ordered {
@@ -308,17 +309,19 @@ public void load() throws IOException {
308309
this.propertiesLoader = new PropertySourcesLoader();
309310
this.profiles = Collections.asLifoQueue(new LinkedList<String>());
310311
this.activatedProfiles = false;
312+
313+
Set<String> initialActiveProfiles = null;
311314
if (this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
312315
// Any pre-existing active profiles set via property sources (e.g. System
313316
// properties) take precedence over those added in config files.
314-
maybeActivateProfiles(
317+
initialActiveProfiles = maybeActivateProfiles(
315318
this.environment.getProperty(ACTIVE_PROFILES_PROPERTY));
316319
}
317320
// Pre-existing active profiles set via Environment.setActiveProfiles()
318321
// are additional profiles and config files are allowed to add more if
319322
// they want to, so don't call addActiveProfiles() here.
320-
List<String> list = new ArrayList<String>(
321-
Arrays.asList(this.environment.getActiveProfiles()));
323+
List<String> list = filterEnvironmentProfiles(initialActiveProfiles != null
324+
? initialActiveProfiles : Collections.<String>emptySet());
322325
// Reverse them so the order is the same as from getProfilesForValue()
323326
// (last one wins when properties are eventually resolved)
324327
Collections.reverse(list);
@@ -404,13 +407,36 @@ private PropertySource<?> loadIntoGroup(String identifier, String location,
404407
return propertySource;
405408
}
406409

407-
private void maybeActivateProfiles(Object value) {
410+
/**
411+
* Return the active profiles that have not been processed yet.
412+
* <p>If a profile is enabled via both {@link #ACTIVE_PROFILES_PROPERTY} and
413+
* {@link ConfigurableEnvironment#addActiveProfile(String)} it needs to be
414+
* filtered so that the {@link #ACTIVE_PROFILES_PROPERTY} value takes
415+
* precedence.
416+
* <p>Concretely, if the "cloud" profile is enabled via the environment,
417+
* it will take less precedence that any profile set via the
418+
* {@link #ACTIVE_PROFILES_PROPERTY}.
419+
* @param initialActiveProfiles the profiles that have been enabled via
420+
* {@link #ACTIVE_PROFILES_PROPERTY}
421+
* @return the additional profiles from the environment to enable
422+
*/
423+
private List<String> filterEnvironmentProfiles(Set<String> initialActiveProfiles) {
424+
List<String> additionalProfiles = new ArrayList<String>();
425+
for (String profile : this.environment.getActiveProfiles()) {
426+
if (!initialActiveProfiles.contains(profile)) {
427+
additionalProfiles.add(profile);
428+
}
429+
}
430+
return additionalProfiles;
431+
}
432+
433+
private Set<String> maybeActivateProfiles(Object value) {
408434
if (this.activatedProfiles) {
409435
if (value != null) {
410436
this.debug.add("Profiles already activated, '" + value
411437
+ "' will not be applied");
412438
}
413-
return;
439+
return Collections.emptySet();
414440
}
415441

416442
Set<String> profiles = getProfilesForValue(value);
@@ -420,6 +446,7 @@ private void maybeActivateProfiles(Object value) {
420446
+ StringUtils.collectionToCommaDelimitedString(profiles));
421447
this.activatedProfiles = true;
422448
}
449+
return profiles;
423450
}
424451

425452
private void addIncludeProfiles(Object value) {

spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2010-2014 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,20 +27,28 @@
2727
import java.util.List;
2828
import java.util.Properties;
2929

30+
import ch.qos.logback.classic.BasicConfigurator;
31+
import ch.qos.logback.classic.Logger;
32+
import ch.qos.logback.classic.LoggerContext;
3033
import org.hamcrest.Description;
3134
import org.hamcrest.Matcher;
3235
import org.hamcrest.TypeSafeDiagnosingMatcher;
3336
import org.junit.After;
37+
import org.junit.Before;
3438
import org.junit.Rule;
3539
import org.junit.Test;
3640
import org.junit.rules.ExpectedException;
41+
import org.slf4j.LoggerFactory;
3742

3843
import org.springframework.boot.SpringApplication;
3944
import org.springframework.boot.context.config.ConfigFileApplicationListener.ConfigurationPropertySources;
4045
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
46+
import org.springframework.boot.context.event.ApplicationPreparedEvent;
4147
import org.springframework.boot.env.EnumerableCompositePropertySource;
4248
import org.springframework.boot.test.EnvironmentTestUtils;
49+
import org.springframework.boot.test.OutputCapture;
4350
import org.springframework.context.ConfigurableApplicationContext;
51+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
4452
import org.springframework.context.annotation.Configuration;
4553
import org.springframework.context.annotation.Profile;
4654
import org.springframework.context.annotation.PropertySource;
@@ -55,13 +63,8 @@
5563
import org.springframework.util.ReflectionUtils;
5664
import org.springframework.util.StringUtils;
5765

58-
import static org.hamcrest.Matchers.contains;
59-
import static org.hamcrest.Matchers.equalTo;
60-
import static org.hamcrest.Matchers.not;
61-
import static org.hamcrest.Matchers.notNullValue;
62-
import static org.hamcrest.Matchers.nullValue;
63-
import static org.junit.Assert.assertEquals;
64-
import static org.junit.Assert.assertThat;
66+
import static org.hamcrest.Matchers.*;
67+
import static org.junit.Assert.*;
6568

6669
/**
6770
* Tests for {@link ConfigFileApplicationListener}.
@@ -81,6 +84,17 @@ public class ConfigFileApplicationListenerTests {
8184
@Rule
8285
public ExpectedException expected = ExpectedException.none();
8386

87+
@Rule
88+
public OutputCapture out = new OutputCapture();
89+
90+
@Before
91+
public void resetLogging() {
92+
LoggerContext loggerContext = ((Logger) LoggerFactory.getLogger(getClass()))
93+
.getLoggerContext();
94+
loggerContext.reset();
95+
BasicConfigurator.configure(loggerContext);
96+
}
97+
8498
@After
8599
public void cleanup() {
86100
System.clearProperty("the.property");
@@ -343,14 +357,63 @@ public void profilePropertiesUsedInPlaceholders() throws Exception {
343357

344358
@Test
345359
public void profilesAddedToEnvironmentAndViaProperty() throws Exception {
360+
// External profile takes precedence over profile added via the environment
346361
EnvironmentTestUtils.addEnvironment(this.environment,
347-
"spring.profiles.active:foo");
362+
"spring.profiles.active:other");
348363
this.environment.addActiveProfile("dev");
349364
this.initializer.onApplicationEvent(this.event);
350-
assertThat(this.environment.getActiveProfiles(),
351-
equalTo(new String[] { "foo", "dev" }));
365+
assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other"));
366+
assertThat(this.environment.getProperty("my.property"),
367+
equalTo("fromotherpropertiesfile"));
368+
validateProfilePrecedence(null, "dev", "other");
369+
}
370+
371+
@Test
372+
public void profilesAddedToEnvironmentAndViaPropertyDuplicate() throws Exception {
373+
EnvironmentTestUtils.addEnvironment(this.environment,
374+
"spring.profiles.active:dev,other");
375+
this.environment.addActiveProfile("dev");
376+
this.initializer.onApplicationEvent(this.event);
377+
assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other"));
378+
assertThat(this.environment.getProperty("my.property"),
379+
equalTo("fromotherpropertiesfile"));
380+
validateProfilePrecedence(null, "dev", "other");
381+
}
382+
383+
@Test
384+
public void profilesAddedToEnvironmentAndViaPropertyDuplicateEnvironmentWins() throws Exception {
385+
EnvironmentTestUtils.addEnvironment(this.environment,
386+
"spring.profiles.active:other,dev");
387+
this.environment.addActiveProfile("other");
388+
this.initializer.onApplicationEvent(this.event);
389+
assertThat(Arrays.asList(this.environment.getActiveProfiles()), containsInAnyOrder("dev", "other"));
352390
assertThat(this.environment.getProperty("my.property"),
353391
equalTo("fromdevpropertiesfile"));
392+
validateProfilePrecedence(null, "other", "dev");
393+
}
394+
395+
private void validateProfilePrecedence(String... profiles) {
396+
this.initializer.onApplicationEvent(new ApplicationPreparedEvent(
397+
new SpringApplication(), new String[0], new AnnotationConfigApplicationContext()));
398+
String log = this.out.toString();
399+
400+
// First make sure that each profile got processed only once
401+
for (String profile : profiles) {
402+
assertThat("Wrong number of occurrences for profile '" + profile + "' --> " + log,
403+
StringUtils.countOccurrencesOf(log, createLogForProfile(profile)), equalTo(1));
404+
}
405+
// Make sure the order of loading is the right one
406+
for (String profile : profiles) {
407+
String line = createLogForProfile(profile);
408+
int index = log.indexOf(line);
409+
assertTrue("Loading profile '" + profile + "' not found in '" + log + "'", index != -1);
410+
log = log.substring(index + line.length(), log.length());
411+
}
412+
}
413+
414+
private String createLogForProfile(String profile) {
415+
String suffix = profile != null ? "-" + profile : "";
416+
return "Loaded config file 'classpath:/application" + suffix + ".properties'";
354417
}
355418

356419
@Test

0 commit comments

Comments
 (0)