Polish "Add profile expression support"

Issue: SPR-12458
This commit is contained in:
Stephane Nicoll 2018-06-13 11:04:23 +02:00
parent e2623b7d35
commit 1f3b4f1863
13 changed files with 258 additions and 83 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Profiles;
/** /**
* Indicates that a component is eligible for registration when one or more * Indicates that a component is eligible for registration when one or more
@ -47,9 +48,14 @@ import org.springframework.core.env.ConfigurableEnvironment;
* *
* <p>If a {@code @Configuration} class is marked with {@code @Profile}, all of the * <p>If a {@code @Configuration} class is marked with {@code @Profile}, all of the
* {@code @Bean} methods and {@link Import @Import} annotations associated with that class * {@code @Bean} methods and {@link Import @Import} annotations associated with that class
* will be bypassed unless one or more of the specified profiles are active. This is * will be bypassed unless one or more of the specified profiles are active. A profile
* analogous to the behavior in Spring XML: if the {@code profile} attribute of the * string may contains a simple profile name (for example {@code "p1"}) or a profile
* {@code beans} element is supplied e.g., {@code <beans profile="p1,p2">}, the * expression. A profile expression allows for more complicated profile logic to be
* expressed, for example {@code "p1 & p2"}. See {@link Profiles#of(String...)} for more
* details about supported formats.
*
* <p>This is analogous to the behavior in Spring XML: if the {@code profile} attribute of
* the {@code beans} element is supplied e.g., {@code <beans profile="p1,p2">}, the
* {@code beans} element will not be parsed unless at least profile 'p1' or 'p2' has been * {@code beans} element will not be parsed unless at least profile 'p1' or 'p2' has been
* activated. Likewise, if a {@code @Component} or {@code @Configuration} class is marked * activated. Likewise, if a {@code @Component} or {@code @Configuration} class is marked
* with {@code @Profile({"p1", "p2"})}, that class will not be registered or processed unless * with {@code @Profile({"p1", "p2"})}, that class will not be registered or processed unless

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.context.annotation; package org.springframework.context.annotation;
import org.springframework.core.env.Profiles;
import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -35,7 +36,7 @@ class ProfileCondition implements Condition {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) { if (attrs != null) {
for (Object value : attrs.get("value")) { for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles((String[]) value)) { if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true; return true;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,6 +54,7 @@ import org.springframework.context.annotation.componentscan.simple.SimpleCompone
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory;
@ -193,7 +194,7 @@ public class ComponentScanAnnotationIntegrationTests {
@Test @Test
public void withAwareTypeFilter() { public void withAwareTypeFilter() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithAwareTypeFilter.class); AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithAwareTypeFilter.class);
assertTrue(ctx.getEnvironment().acceptsProfiles("the-filter-ran")); assertTrue(ctx.getEnvironment().acceptsProfiles(Profiles.of("the-filter-ran")));
} }
@Test @Test

View File

@ -322,6 +322,23 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
} }
} }
@Override
@Deprecated
public boolean acceptsProfiles(String... profiles) {
Assert.notEmpty(profiles, "Must specify at least one profile");
for (String profile : profiles) {
if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
if (!isProfileActive(profile.substring(1))) {
return true;
}
}
else if (isProfileActive(profile)) {
return true;
}
}
return false;
}
@Override @Override
public boolean acceptsProfiles(Profiles profiles) { public boolean acceptsProfiles(Profiles profiles) {
Assert.notNull(profiles, "Profiles must not be null"); Assert.notNull(profiles, "Profiles must not be null");

View File

@ -97,18 +97,22 @@ public interface Environment extends PropertyResolver {
/** /**
* Return whether one or more of the given profiles is active or, in the case of no * Return whether one or more of the given profiles is active or, in the case of no
* explicit active profiles, whether one or more of the given profiles is included in * explicit active profiles, whether one or more of the given profiles is included in
* the set of default profiles. Profiles can simple indicators ('{@code p1}', * the set of default profiles. If a profile begins with '!' the logic is inverted,
* {@code !p1}) or more complex boolean expressions. See {@link Profiles#of(String...)} * i.e. the method will return true if the given profile is <em>not</em> active.
* for syntax details. * For example, <pre class="code">env.acceptsProfiles("p1", "!p2")</pre> will
* return {@code true} if profile 'p1' is active or 'p2' is not active.
* @throws IllegalArgumentException if called with zero arguments
* or if any profile is {@code null}, empty or whitespace-only
* @see #getActiveProfiles * @see #getActiveProfiles
* @see #getDefaultProfiles * @see #getDefaultProfiles
* @see #acceptsProfiles(Profiles)
* @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)}
*/ */
default boolean acceptsProfiles(String... profiles) { @Deprecated
return acceptsProfiles(Profiles.of(profiles)); boolean acceptsProfiles(String... profiles);
}
/** /**
* Returns whether the active profiles match the given {@link Profiles} set. * Return whether the active profiles match the given {@link Profiles} predicate.
*/ */
boolean acceptsProfiles(Profiles profiles); boolean acceptsProfiles(Profiles profiles);

View File

@ -16,30 +16,32 @@
package org.springframework.core.env; package org.springframework.core.env;
import java.util.function.Predicate;
/** /**
* A set of profiles that may be {@link Environment#acceptsProfiles(Profiles) accepted} by * Profile predicate that may be {@link Environment#acceptsProfiles(Profiles) accepted} by
* an {@link Environment}. * an {@link Environment}.
* <p> * <p>
* May be implemented directly or, more usually, created using the {@link #of(String...) * May be implemented directly or, more usually, created using the {@link #of(String...)
* of(...)} factory method. * of(...)} factory method.
* *
* @author Phillip Webb * @author Phillip Webb
* @since 5.0 * @since 5.1
* @see #of(String...)
*/ */
@FunctionalInterface @FunctionalInterface
public interface Profiles { public interface Profiles {
/** /**
* Test if this profile set matches against given active profiles. * Test if this profile predicate matches against given active profiles.
* @param activeProfiles test whether a given profile is currently active
*/ */
boolean matches(ActiveProfiles activeProfiles); boolean matches(Predicate<String> activeProfiles);
/** /**
* Return a new {@link Profiles} instance that checks for matches against the given * Return a new {@link Profiles} instance that checks for matches against the given
* profile strings. The returned instance will * profile strings. The returned instance will
* {@link Profiles#matches(ActiveProfiles) matches} if any one of the given profile * {@link Profiles#matches(Predicate)} match} if any one of the given profile strings
* strings match. * match.
* <p> * <p>
* A profile string may contains a simple profile name (for example * A profile string may contains a simple profile name (for example
* {@code "production"}) or a profile expression. A profile expression allows for more * {@code "production"}) or a profile expression. A profile expression allows for more
@ -59,18 +61,8 @@ public interface Profiles {
* @param profiles the profiles to include * @param profiles the profiles to include
* @return a new {@link Profiles} instance * @return a new {@link Profiles} instance
*/ */
public static Profiles of(String... profiles) { static Profiles of(String... profiles) {
return ProfilesParser.parse(profiles); return ProfilesParser.parse(profiles);
} }
/**
* The current set of active profiles.
*/
interface ActiveProfiles {
/**
* Tests if given profile is currently active.
*/
boolean contains(String profile);
}
} }

View File

@ -19,10 +19,10 @@ package org.springframework.core.env;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.core.env.Profiles.ActiveProfiles;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -30,7 +30,6 @@ import org.springframework.util.StringUtils;
* Internal parser used by {@link Profiles#of}. * Internal parser used by {@link Profiles#of}.
* *
* @author Phillip Webb * @author Phillip Webb
* @since 5.0
*/ */
class ProfilesParser { class ProfilesParser {
@ -55,7 +54,7 @@ class ProfilesParser {
Operator operator = null; Operator operator = null;
while (tokens.hasMoreTokens()) { while (tokens.hasMoreTokens()) {
String token = tokens.nextToken().trim(); String token = tokens.nextToken().trim();
if(token.isEmpty()) { if (token.isEmpty()) {
continue; continue;
} }
switch (token) { switch (token) {
@ -86,7 +85,8 @@ class ProfilesParser {
return merge(expression, elements, operator); return merge(expression, elements, operator);
} }
private static Profiles merge(String expression, List<Profiles> elements, Operator operator) { private static Profiles merge(String expression, List<Profiles> elements,
Operator operator) {
assertWellFormed(expression, !elements.isEmpty()); assertWellFormed(expression, !elements.isEmpty());
if (elements.size() == 1) { if (elements.size() == 1) {
return elements.get(0); return elements.get(0);
@ -115,16 +115,17 @@ class ProfilesParser {
} }
private static Profiles equals(String profile) { private static Profiles equals(String profile) {
return (activeProfile) -> activeProfile.contains(profile); return (activeProfile) -> activeProfile.test(profile);
} }
private static Predicate<Profiles> isMatch(ActiveProfiles activeProfile) { private static Predicate<Profiles> isMatch(Predicate<String> activeProfile) {
return (profiles) -> profiles.matches(activeProfile); return (profiles) -> profiles.matches(activeProfile);
} }
enum Operator { enum Operator {
AND, OR AND,
}; OR
}
private static class ParsedProfiles implements Profiles { private static class ParsedProfiles implements Profiles {
@ -138,7 +139,7 @@ class ProfilesParser {
} }
@Override @Override
public boolean matches(ActiveProfiles activeProfiles) { public boolean matches(Predicate<String> activeProfiles) {
for (Profiles candidate : this.parsed) { for (Profiles candidate : this.parsed) {
if (candidate.matches(activeProfiles)) { if (candidate.matches(activeProfiles)) {
return true; return true;
@ -149,7 +150,9 @@ class ProfilesParser {
@Override @Override
public String toString() { public String toString() {
return StringUtils.arrayToCommaDelimitedString(this.expressions); return StringUtils.arrayToDelimitedString(this.expressions, " or ");
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2011 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -38,7 +38,7 @@ public class CustomEnvironmentTests {
@Test @Test
public void control() { public void control() {
Environment env = new AbstractEnvironment() { }; Environment env = new AbstractEnvironment() { };
assertThat(env.acceptsProfiles(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME), is(true)); assertThat(env.acceptsProfiles(defaultProfile()), is(true));
} }
@Test @Test
@ -51,7 +51,7 @@ public class CustomEnvironmentTests {
} }
Environment env = new CustomEnvironment(); Environment env = new CustomEnvironment();
assertThat(env.acceptsProfiles(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME), is(false)); assertThat(env.acceptsProfiles(defaultProfile()), is(false));
} }
@Test @Test
@ -64,8 +64,8 @@ public class CustomEnvironmentTests {
} }
Environment env = new CustomEnvironment(); Environment env = new CustomEnvironment();
assertThat(env.acceptsProfiles(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME), is(false)); assertThat(env.acceptsProfiles(defaultProfile()), is(false));
assertThat(env.acceptsProfiles("rd1"), is(true)); assertThat(env.acceptsProfiles(Profiles.of("rd1")), is(true));
} }
@Test @Test
@ -79,28 +79,32 @@ public class CustomEnvironmentTests {
} }
ConfigurableEnvironment env = new CustomEnvironment(); ConfigurableEnvironment env = new CustomEnvironment();
assertThat(env.acceptsProfiles(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME), is(false)); assertThat(env.acceptsProfiles(defaultProfile()), is(false));
assertThat(env.acceptsProfiles("rd1", "rd2"), is(true)); assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2")), is(true));
// finally, issue additional assertions to cover all combinations of calling these // finally, issue additional assertions to cover all combinations of calling these
// methods, however unlikely. // methods, however unlikely.
env.setDefaultProfiles("d1"); env.setDefaultProfiles("d1");
assertThat(env.acceptsProfiles("rd1", "rd2"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2")), is(false));
assertThat(env.acceptsProfiles("d1"), is(true)); assertThat(env.acceptsProfiles(Profiles.of("d1")), is(true));
env.setActiveProfiles("a1", "a2"); env.setActiveProfiles("a1", "a2");
assertThat(env.acceptsProfiles("d1"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("d1")), is(false));
assertThat(env.acceptsProfiles("a1", "a2"), is(true)); assertThat(env.acceptsProfiles(Profiles.of("a1 | a2")), is(true));
env.setActiveProfiles(); env.setActiveProfiles();
assertThat(env.acceptsProfiles("d1"), is(true)); assertThat(env.acceptsProfiles(Profiles.of("d1")), is(true));
assertThat(env.acceptsProfiles("a1", "a2"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("a1 | a2")), is(false));
env.setDefaultProfiles(); env.setDefaultProfiles();
assertThat(env.acceptsProfiles(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME), is(false)); assertThat(env.acceptsProfiles(defaultProfile()), is(false));
assertThat(env.acceptsProfiles("rd1", "rd2"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2")), is(false));
assertThat(env.acceptsProfiles("d1"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("d1")), is(false));
assertThat(env.acceptsProfiles("a1", "a2"), is(false)); assertThat(env.acceptsProfiles(Profiles.of("a1 | a2")), is(false));
}
private Profiles defaultProfile() {
return Profiles.of(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME);
} }

View File

@ -73,8 +73,14 @@ public class DummyEnvironment implements Environment {
return null; return null;
} }
@Override
public boolean acceptsProfiles(String... profiles) {
return false;
}
@Override @Override
public boolean acceptsProfiles(Profiles profiles) { public boolean acceptsProfiles(Profiles profiles) {
return false; return false;
} }
} }

View File

@ -21,12 +21,13 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.core.env.Profiles.ActiveProfiles;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -35,6 +36,7 @@ import static org.junit.Assert.*;
* Tests for {@link Profiles}. * Tests for {@link Profiles}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll
*/ */
public class ProfilesTests { public class ProfilesTests {
@ -56,10 +58,10 @@ public class ProfilesTests {
} }
@Test @Test
public void ofNullElement() throws Exception { public void ofNullElement() {
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("must contain text"); this.thrown.expectMessage("must contain text");
Profiles.of((String)null); Profiles.of((String) null);
} }
@Test @Test
@ -103,22 +105,32 @@ public class ProfilesTests {
} }
@Test @Test
public void ofSingleExpression() throws Exception { public void ofSingleExpression() {
Profiles profiles = Profiles.of("(spring)"); Profiles profiles = Profiles.of("(spring)");
assertTrue(profiles.matches(new MockActiveProfiles("spring"))); assertTrue(profiles.matches(new MockActiveProfiles("spring")));
assertFalse(profiles.matches(new MockActiveProfiles("framework"))); assertFalse(profiles.matches(new MockActiveProfiles("framework")));
} }
@Test @Test
public void ofSingleInvertedExpression() throws Exception { public void ofSingleInvertedExpression() {
Profiles profiles = Profiles.of("(!spring)"); Profiles profiles = Profiles.of("(!spring)");
assertFalse(profiles.matches(new MockActiveProfiles("spring"))); assertFalse(profiles.matches(new MockActiveProfiles("spring")));
assertTrue(profiles.matches(new MockActiveProfiles("framework"))); assertTrue(profiles.matches(new MockActiveProfiles("framework")));
} }
@Test @Test
public void ofOrExpression() throws Exception { public void ofOrExpression() {
Profiles profiles = Profiles.of("(spring | framework)"); Profiles profiles = Profiles.of("(spring | framework)");
assertOrExpression(profiles);
}
@Test
public void ofOrExpressionWithoutSpace() {
Profiles profiles = Profiles.of("(spring|framework)");
assertOrExpression(profiles);
}
private void assertOrExpression(Profiles profiles) {
assertTrue(profiles.matches(new MockActiveProfiles("spring"))); assertTrue(profiles.matches(new MockActiveProfiles("spring")));
assertTrue(profiles.matches(new MockActiveProfiles("framework"))); assertTrue(profiles.matches(new MockActiveProfiles("framework")));
assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework"))); assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
@ -126,17 +138,24 @@ public class ProfilesTests {
} }
@Test @Test
public void ofAndExpression() throws Exception { public void ofAndExpression() {
Profiles profiles = Profiles.of("(spring & framework)"); Profiles profiles = Profiles.of("(spring & framework)");
assertFalse(profiles.matches(new MockActiveProfiles("spring"))); assertAndExpression(profiles);
assertFalse(profiles.matches(new MockActiveProfiles("framework")));
assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
assertFalse(profiles.matches(new MockActiveProfiles("java")));
} }
@Test @Test
public void ofAndExpressionWithoutBraces() throws Exception { public void ofAndExpressionWithoutSpace() {
Profiles profiles = Profiles.of("spring&framework)");
assertAndExpression(profiles);
}
@Test
public void ofAndExpressionWithoutBraces() {
Profiles profiles = Profiles.of("spring & framework"); Profiles profiles = Profiles.of("spring & framework");
assertAndExpression(profiles);
}
private void assertAndExpression(Profiles profiles) {
assertFalse(profiles.matches(new MockActiveProfiles("spring"))); assertFalse(profiles.matches(new MockActiveProfiles("spring")));
assertFalse(profiles.matches(new MockActiveProfiles("framework"))); assertFalse(profiles.matches(new MockActiveProfiles("framework")));
assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework"))); assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
@ -144,8 +163,18 @@ public class ProfilesTests {
} }
@Test @Test
public void ofNotAndExpression() throws Exception { public void ofNotAndExpression() {
Profiles profiles = Profiles.of("!(spring & framework)"); Profiles profiles = Profiles.of("!(spring & framework)");
assertOfNotAndExpression(profiles);
}
@Test
public void ofNotAndExpressionWithoutSpace() {
Profiles profiles = Profiles.of("!(spring&framework)");
assertOfNotAndExpression(profiles);
}
private void assertOfNotAndExpression(Profiles profiles) {
assertTrue(profiles.matches(new MockActiveProfiles("spring"))); assertTrue(profiles.matches(new MockActiveProfiles("spring")));
assertTrue(profiles.matches(new MockActiveProfiles("framework"))); assertTrue(profiles.matches(new MockActiveProfiles("framework")));
assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework"))); assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework")));
@ -153,8 +182,18 @@ public class ProfilesTests {
} }
@Test @Test
public void ofNotOrExpression() throws Exception { public void ofNotOrExpression() {
Profiles profiles = Profiles.of("!(spring | framework)"); Profiles profiles = Profiles.of("!(spring | framework)");
assertOfNotOrExpression(profiles);
}
@Test
public void ofNotOrExpressionWithoutSpace() {
Profiles profiles = Profiles.of("!(spring|framework)");
assertOfNotOrExpression(profiles);
}
private void assertOfNotOrExpression(Profiles profiles) {
assertFalse(profiles.matches(new MockActiveProfiles("spring"))); assertFalse(profiles.matches(new MockActiveProfiles("spring")));
assertFalse(profiles.matches(new MockActiveProfiles("framework"))); assertFalse(profiles.matches(new MockActiveProfiles("framework")));
assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework"))); assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework")));
@ -162,8 +201,18 @@ public class ProfilesTests {
} }
@Test @Test
public void ofComplex() throws Exception { public void ofComplexExpression() {
Profiles profiles = Profiles.of("(spring & framework) | (spring & java)"); Profiles profiles = Profiles.of("(spring & framework) | (spring & java)");
assertComplexExpression(profiles);
}
@Test
public void ofComplexExpressionWithoutSpace() {
Profiles profiles = Profiles.of("(spring&framework)|(spring&java)");
assertComplexExpression(profiles);
}
private void assertComplexExpression(Profiles profiles) {
assertFalse(profiles.matches(new MockActiveProfiles("spring"))); assertFalse(profiles.matches(new MockActiveProfiles("spring")));
assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework"))); assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
assertTrue(profiles.matches(new MockActiveProfiles("spring", "java"))); assertTrue(profiles.matches(new MockActiveProfiles("spring", "java")));
@ -171,12 +220,18 @@ public class ProfilesTests {
} }
@Test @Test
public void malformedExpressions() throws Exception { public void malformedExpressions() {
assertMalformed(() -> Profiles.of("(")); assertMalformed(() -> Profiles.of("("));
assertMalformed(() -> Profiles.of(")")); assertMalformed(() -> Profiles.of(")"));
assertMalformed(() -> Profiles.of("a & b | c")); assertMalformed(() -> Profiles.of("a & b | c"));
} }
@Test
public void sensibleToString() {
assertEquals("spring & framework or java | kotlin",
Profiles.of("spring & framework", "java | kotlin").toString());
}
private void assertMalformed(Supplier<Profiles> supplier) { private void assertMalformed(Supplier<Profiles> supplier) {
try { try {
supplier.get(); supplier.get();
@ -187,7 +242,7 @@ public class ProfilesTests {
} }
} }
private static class MockActiveProfiles implements ActiveProfiles { private static class MockActiveProfiles implements Predicate<String> {
private Set<String> activeProfiles; private Set<String> activeProfiles;
@ -210,13 +265,14 @@ public class ProfilesTests {
@Override @Override
public boolean contains(String profile) { public boolean test(String profile) {
if (!StringUtils.hasText(profile) || profile.charAt(0) == '!') { if (!StringUtils.hasText(profile) || profile.charAt(0) == '!') {
throw new IllegalArgumentException("Invalid profile [" + profile + "]"); throw new IllegalArgumentException("Invalid profile [" + profile + "]");
} }
return (this.activeProfiles.contains(profile) return (this.activeProfiles.contains(profile)
|| (this.activeProfiles.isEmpty() && this.defaultProfiles.contains(profile))); || (this.activeProfiles.isEmpty() && this.defaultProfiles.contains(profile)));
} }
} }
} }

View File

@ -326,12 +326,12 @@ public class StandardEnvironmentTests {
} }
@Test @Test
public void acceptsProfiles_withProfileExpression() throws Exception { public void acceptsProfiles_withProfileExpression() {
assertThat(environment.acceptsProfiles("p1 & p2"), is(false)); assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2")), is(false));
environment.addActiveProfile("p1"); environment.addActiveProfile("p1");
assertThat(environment.acceptsProfiles("p1 & p2"), is(false)); assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2")), is(false));
environment.addActiveProfile("p2"); environment.addActiveProfile("p2");
assertThat(environment.acceptsProfiles("p1 & p2"), is(true)); assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2")), is(true));
} }
@Test @Test

View File

@ -7664,6 +7664,22 @@ straight JNDI `InitialContext` usage shown above, but not the `JndiObjectFactory
variant which would force you to declare the return type as the `FactoryBean` type. variant which would force you to declare the return type as the `FactoryBean` type.
==== ====
The profile string may contains a simple profile name (for example `production`) or a
profile expression. A profile expression allows for more complicated profile logic to be
expressed, for example `production & us-east`. The following operators are supported in
profile expressions:
* `!` - A logical not of the profile
* `&` - A logical and of the profiles
* `|` - A logical or of the profiles
[NOTE]
====
The `&` and `|` operators may not be mixed without using parentheses. For example
`production & us-east | eu-central` is not a valid expression, it must be expressed as
`production & (us-east | eu-central)`.
====
`@Profile` can be used as a <<beans-meta-annotations,meta-annotation>> for the purpose `@Profile` can be used as a <<beans-meta-annotations,meta-annotation>> for the purpose
of creating a custom _composed annotation_. The following example defines a custom of creating a custom _composed annotation_. The following example defines a custom
`@Production` annotation that can be used as a drop-in replacement for `@Production` annotation that can be used as a drop-in replacement for
@ -7804,6 +7820,35 @@ The `spring-bean.xsd` has been constrained to allow such elements only as the
last ones in the file. This should help provide flexibility without incurring last ones in the file. This should help provide flexibility without incurring
clutter in the XML files. clutter in the XML files.
[NOTE]
====
The XML counterpart does not support profile expressions described above. It is possible
however to negate a profile using the `!` operator. It is also possible to apply a logical
and by nesting the profiles:
[source,xml,indent=0]
[subs="verbatim,quotes"]
----
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>
----
In the example above, the `dataSource` bean will be exposed if both the `production` and
`us-east` profiles are active.
====
[[beans-definition-profiles-enable]] [[beans-definition-profiles-enable]]
==== Activating a profile ==== Activating a profile

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -305,6 +305,37 @@ public class EnvironmentSystemIntegrationTests {
assertThat("should not have transitive bean", ctx.containsBean(TRANSITIVE_BEAN_NAME), is(false)); assertThat("should not have transitive bean", ctx.containsBean(TRANSITIVE_BEAN_NAME), is(false));
} }
@Test
public void annotationConfigApplicationContext_withProfileExpressionMatchOr() {
testProfileExpression(true, "p3");
}
@Test
public void annotationConfigApplicationContext_withProfileExpressionMatchAnd() {
testProfileExpression(true, "p1", "p2");
}
@Test
public void annotationConfigApplicationContext_withProfileExpressionNoMatchAnd() {
testProfileExpression(false, "p1");
}
@Test
public void annotationConfigApplicationContext_withProfileExpressionNoMatchNone() {
testProfileExpression(false, "p4");
}
private void testProfileExpression(boolean expected, String... activeProfiles) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
StandardEnvironment environment = new StandardEnvironment();
environment.setActiveProfiles(activeProfiles);
ctx.setEnvironment(environment);
ctx.register(ProfileExpressionConfig.class);
ctx.refresh();
assertThat("wrong presence of expression bean",
ctx.containsBean("expressionBean"), is(expected));
}
@Test @Test
public void webApplicationContext() { public void webApplicationContext() {
GenericWebApplicationContext ctx = new GenericWebApplicationContext(newBeanFactoryWithEnvironmentAwareBean()); GenericWebApplicationContext ctx = new GenericWebApplicationContext(newBeanFactoryWithEnvironmentAwareBean());
@ -644,6 +675,15 @@ public class EnvironmentSystemIntegrationTests {
} }
} }
@Profile("(p1 & p2) | p3")
@Configuration
static class ProfileExpressionConfig {
@Bean
public Object expressionBean() {
return new Object();
}
}
/** /**
* Constants used both locally and in scan* sub-packages * Constants used both locally and in scan* sub-packages