From eee260fc03731df41b57311564cbff9bb7d1d2c9 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 21 Jul 2020 23:57:04 -0700 Subject: [PATCH] Add support for profile groups Add support for profile groups so that users can combine a number of fine-grained profiles into a single logical group. Closes gh-22522 --- .../docs/asciidoc/spring-boot-features.adoc | 21 ++++++ .../boot/context/config/Profiles.java | 14 +++- .../boot/context/config/ProfilesTests.java | 67 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 4a0848d98e..6d37c67a1b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -1711,6 +1711,27 @@ For example, when an application with the following properties is run by using t +[[boot-features-profiles-groups]] +=== Profile Groups +Occasionally the profiles that you define and use in your application are too fine-grained and become cumbersome to use. +For example, you might have `proddb` and `prodmq` profiles that you use to enable database and messaging features independently. + +To help with this, Spring Boot lets you define profile groups. +A profile group allows you to define a logical name for a related group of profiles. + +For example, we can create a `production` group that consists of our `proddb` and `prodmq` profiles. + +[source,yaml,indent=0] +---- + spring.profiles.group.production: + - proddb + - prodmq +---- + +Our application can now be started using `--spring.profiles.active=production` to active the `production`, `proddb` and `prodmq` profiles in one hit. + + + [[boot-features-programmatically-setting-profiles]] === Programmatically Setting Profiles You can programmatically set active profiles by calling `SpringApplication.setAdditionalProfiles(...)` before your application runs. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java index 0726fb5716..92d48b905e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/Profiles.java @@ -28,11 +28,15 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.ResolvableType; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.style.ToStringCreator; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -45,10 +49,15 @@ import org.springframework.util.StringUtils; */ public class Profiles implements Iterable { + private static final Bindable> STRING_STRINGS_MAP = Bindable + .of(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class)); + private static final Set UNSET_ACTIVE = Collections.emptySet(); private static final Set UNSET_DEFAULT = Collections.singleton("default"); + private final MultiValueMap groups; + private final List activeProfiles; private final List defaultProfiles; @@ -63,6 +72,7 @@ public class Profiles implements Iterable { * @param additionalProfiles and additional active profiles */ Profiles(Environment environment, Binder binder, Collection additionalProfiles) { + this.groups = binder.bind("spring.profiles.group", STRING_STRINGS_MAP).orElseGet(LinkedMultiValueMap::new); this.activeProfiles = asUniqueItemList(get(environment, binder, environment::getActiveProfiles, AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, UNSET_ACTIVE), additionalProfiles); this.defaultProfiles = asUniqueItemList(get(environment, binder, environment::getDefaultProfiles, @@ -94,7 +104,9 @@ public class Profiles implements Iterable { asReversedList((!activeProfiles.isEmpty()) ? activeProfiles : defaultProfiles).forEach(stack::push); Set acceptedProfiles = new LinkedHashSet<>(); while (!stack.isEmpty()) { - acceptedProfiles.add(stack.pop()); + String current = stack.pop(); + acceptedProfiles.add(current); + asReversedList(this.groups.get(current)).forEach(stack::push); } return asUniqueItemList(StringUtils.toStringArray(acceptedProfiles)); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java index 24f69c53e8..c147dd432d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ProfilesTests.java @@ -262,6 +262,17 @@ class ProfilesTests { assertThat(profiles.isAccepted("x")).isFalse(); } + @Test + void iteratorWithProfileGroups() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.active", "a,b,c"); + environment.setProperty("spring.profiles.group.a", "e,f"); + environment.setProperty("spring.profiles.group.e", "x,y"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles).containsExactly("a", "e", "x", "y", "f", "b", "c"); + } + @Test void iteratorWithProfileGroupsAndNoActive() { MockEnvironment environment = new MockEnvironment(); @@ -272,4 +283,60 @@ class ProfilesTests { assertThat(profiles).containsExactly("default"); } + @Test + void iteratorWithProfileGroupsForDefault() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.group.default", "e,f"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles).containsExactly("default", "e", "f"); + } + + @Test + void getAcceptedWithProfileGroups() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.active", "a,b,c"); + environment.setProperty("spring.profiles.group.a", "e,f"); + environment.setProperty("spring.profiles.group.e", "x,y"); + environment.setDefaultProfiles("g", "h", "i"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles.getAccepted()).containsExactly("a", "e", "x", "y", "f", "b", "c"); + } + + @Test + void getAcceptedWhenNoActiveAndDefaultWithGroups() { + MockEnvironment environment = new MockEnvironment(); + environment.setDefaultProfiles("d", "e", "f"); + environment.setProperty("spring.profiles.group.e", "x,y"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles.getAccepted()).containsExactly("d", "e", "x", "y", "f"); + } + + @Test + void isAcceptedWithGroupsReturnsTrue() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.profiles.active", "a,b,c"); + environment.setProperty("spring.profiles.group.a", "e,f"); + environment.setProperty("spring.profiles.group.e", "x,y"); + environment.setDefaultProfiles("g", "h", "i"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles.isAccepted("a")).isTrue(); + assertThat(profiles.isAccepted("e")).isTrue(); + assertThat(profiles.isAccepted("g")).isFalse(); + } + + @Test + void isAcceptedWhenNoActiveAndDefaultWithGroupsContainsProfileReturnsTrue() { + MockEnvironment environment = new MockEnvironment(); + environment.setDefaultProfiles("d", "e", "f"); + environment.setProperty("spring.profiles.group.e", "x,y"); + Binder binder = Binder.get(environment); + Profiles profiles = new Profiles(environment, binder, null); + assertThat(profiles.isAccepted("d")).isTrue(); + assertThat(profiles.isAccepted("x")).isTrue(); + } + }