50

Spring Profile annotation allows you to select profiles. However if you read documentation it only allows you to select more than one profile with OR operation. If you specify @Profile("A", "B") then your bean will be up if either profile A or profile B is active.

Our use case is different we want to support TEST and PROD versions of multiple configurations. Therefore sometimes we want to autowire the bean only if both profiles TEST and CONFIG1 are active.

Is there any way to do it with Spring? What would be the simplest way?

Artem
  • 7,275
  • 15
  • 57
  • 97
  • 1
    well in docs its mentioned as `and/or` behaviour for `@Profile("a","b")`. Isn't that what you are looking for? docs - `Likewise, if a @Component or @Configuration class is marked with @Profile({"p1", "p2"}), that class will not be registered/processed unless profiles 'p1' and/or 'p2' have been activated.` – Bond - Java Bond Nov 21 '14 at 06:11
  • @JavaBond which means that it is "OR" operator and not "AND". They just wanted to point out explicitly that it is not exclusive or (xor) – Artem Nov 21 '14 at 06:22
  • 1
    I opened a ticket for Spring Source to support "AND" operator for Profile annotation: https://jira.spring.io/browse/SPR-12458 – Artem Nov 21 '14 at 06:23
  • okies. lets see what spring team has to say. – Bond - Java Bond Nov 21 '14 at 06:34
  • 2
    They accepted the ticket and apparently going to do it at some point. – Artem Nov 22 '14 at 02:23

7 Answers7

42

Since Spring 5.1 (incorporated in Spring Boot 2.1) it is possible to use a profile expression inside profile string annotation. So:

In Spring 5.1 (Spring Boot 2.1) and above it is as easy as:

@Component
@Profile("TEST & CONFIG1")
public class MyComponent {}

Spring 4.x and 5.0.x:

  • Approach 1: answered by @Mithun, it covers perfectly your case of converting OR into AND in your profile annotation whenever you annotate the Spring Bean also with his Condition class implementation. But I want to offer another approach that nobody proposed that has its pro's and con's.

  • Approach 2: Just use @Conditional and create as many Condition implementations as combinations needed. It has the con of having to create as many implementations as combinations but if you don't have many combinations, in my opinion, it is a more concise solution and it offers more flexibility and the chance of implementing more complex logical resolutions.

The implementation of Approach 2 would be as follows.

Your Spring Bean:

@Component
@Conditional(value = { TestAndConfig1Profiles.class })
public class MyComponent {}

TestAndConfig1Profiles implementation:

public class TestAndConfig1Profiles implements Condition {
    @Override
    public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().acceptsProfiles("TEST")
                    && context.getEnvironment().acceptsProfiles("CONFIG1");
    }
}

With this approach you could easily cover more complex logical situations like for example:

(TEST & CONFIG1) | (TEST & CONFIG3)

Just wanted to give an updated answer to your question and complement other answers.

f-CJ
  • 4,235
  • 2
  • 30
  • 28
  • It seems that the expression @Profile("TEST & CONFIG1") does NOT work at bean (method) level in Spring 5.1 yet. – agodinhost Dec 22 '18 at 22:39
  • @agodinhost It has to be at class level – f-CJ Dec 23 '18 at 06:37
  • Yes, I used the @ConditionalOnExpression at method level using the expression "#{environment.acceptsProfiles('spring') && environment.acceptsProfiles('oracle')}" and it worked 100%. Would be really good to have this Profile Expression at method level also. IMHO does not make sense to be different at method level. – agodinhost Dec 23 '18 at 16:30
  • 1
    Just a tiny update, use acceptsProfiles(Profiles.of("TEST")) as the overload for String [] is now deprecated. – David Bradley Dec 17 '20 at 19:53
27

Since Spring does not provide the AND feature out of the box. I would suggest the following strategy:

Currently @Profile annotation has a conditional annotation @Conditional(ProfileCondition.class). In ProfileCondition.class it iterates through the profiles and checks if the profile is active. Similarly you could create your own conditional implementation and restrict registering the bean. e.g.

public class MyProfileCondition implements Condition {

    @Override
    public boolean matches(final ConditionContext context,
            final AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            final MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (final Object value : attrs.get("value")) {
                    final String activeProfiles = context.getEnvironment().getProperty("spring.profiles.active");

                    for (final String profile : (String[]) value) {
                        if (!activeProfiles.contains(profile)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        }
        return true;
    }

}

In your class:

@Component
@Profile("dev")
@Conditional(value = { MyProfileCondition.class })
public class DevDatasourceConfig

NOTE: I have not checked for all the corner cases (like null, length checks etc). But, this direction could help.

Mithun
  • 7,747
  • 6
  • 52
  • 68
9

A little bit improved version of @Mithun answer:

public class AndProfilesCondition implements Condition {

public static final String VALUE = "value";
public static final String DEFAULT_PROFILE = "default";

@Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() == null) {
        return true;
    }
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs == null) {
        return true;
    }
    String[] activeProfiles = context.getEnvironment().getActiveProfiles();
    String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
    Set<String> allowedProfiles = new HashSet<>(1);
    Set<String> restrictedProfiles = new HashSet<>(1);
    for (String nextDefinedProfile : definedProfiles) {
        if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
            restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
            continue;
        }
        allowedProfiles.add(nextDefinedProfile);
    }
    int activeAllowedCount = 0;
    for (String nextActiveProfile : activeProfiles) {
        // quick exit when default profile is active and allowed profiles is empty
        if (DEFAULT_PROFILE.equals(nextActiveProfile) && allowedProfiles.isEmpty()) {
            continue;
        }
        // quick exit when one of active profiles is restricted
        if (restrictedProfiles.contains(nextActiveProfile)) {
            return false;
        }
        // just go ahead when there is no allowed profiles (just need to check that there is no active restricted profiles)
        if (allowedProfiles.isEmpty()) {
            continue;
        }
        if (allowedProfiles.contains(nextActiveProfile)) {
            activeAllowedCount++;
        }
    }
    return activeAllowedCount == allowedProfiles.size();
}

}

Was unable to post it in the comments.

Vova Rozhkov
  • 1,582
  • 2
  • 19
  • 27
7

Yet another option is to play on the Class/Method level allowed by the @Profile annotation. Not as flexible as implementing MyProfileCondition but quick and clean if it suits your case.

e.g. this won't start when FAST & DEV are both active, but will if only DEV is:

@Configuration
@Profile("!" + SPRING_PROFILE_FAST)
public class TomcatLogbackAccessConfiguration {

    @Bean
    @Profile({SPRING_PROFILE_DEVELOPMENT, SPRING_PROFILE_STAGING})
    public EmbeddedServletContainerCustomizer containerCustomizer() {
coolnodje
  • 789
  • 1
  • 7
  • 20
7

Another kind of trick but might work in many scenarios is put @Profile annotation on @Configuration and the other @Profile on @Bean - that creates logical AND between 2 profiles in java-based spring config.

@Configuration
@Profile("Profile1")
public class TomcatLogbackAccessConfiguration {

   @Bean
   @Profile("Profile2")
   public EmbeddedServletContainerCustomizer containerCustomizer() {
Martin Tlachač
  • 379
  • 4
  • 17
6

If you have already marked a configuration class or bean method with @Profile annotation, it is simple to check for additional profiles (e.g. for AND condition) with Environment.acceptsProfiles()

@Autowired Environment env;

@Profile("profile1")
@Bean
public MyBean myBean() {
    if( env.acceptsProfiles("profile2") ) {
        return new MyBean();
    }
    else {
        return null;
    }
}
Sergey Shcherbakov
  • 4,534
  • 4
  • 40
  • 65
6

I improved @rozhoc's answer since that answer did not account for the fact that no profile is equivalent to 'default' when it comes to using @Profile. Also, conditions that I wanted were !default && !a which @rozhoc's code did not handle properly. Finally I used some Java8 and show only the matches method for brevity.

@Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() == null) {
        return true;
    }
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs == null) {
        return true;
    }

    Set<String> activeProfilesSet = Arrays.stream(context.getEnvironment().getActiveProfiles()).collect(Collectors.toSet());
    String[] definedProfiles = (String[]) attrs.getFirst(VALUE);
    Set<String> allowedProfiles = new HashSet<>(1);
    Set<String> restrictedProfiles = new HashSet<>(1);
    if (activeProfilesSet.size() == 0) {
        activeProfilesSet.add(DEFAULT_PROFILE);  // no profile is equivalent in @Profile terms to "default"
    }
    for (String nextDefinedProfile : definedProfiles) {
        if (!nextDefinedProfile.isEmpty() && nextDefinedProfile.charAt(0) == '!') {
            restrictedProfiles.add(nextDefinedProfile.substring(1, nextDefinedProfile.length()));
            continue;
        }
        allowedProfiles.add(nextDefinedProfile);
    }
    boolean allowed = true;
    for (String allowedProfile : allowedProfiles) {
        allowed = allowed && activeProfilesSet.contains(allowedProfile);
    }
    boolean restricted = true;
    for (String restrictedProfile : restrictedProfiles) {
        restricted = restricted && !activeProfilesSet.contains(restrictedProfile);
    }
    return allowed && restricted;
}

Here is how you actually use it in case that was confusing as well:

@Profile({"!default", "!a"})
@Conditional(value={AndProfilesCondition.class})
RubesMN
  • 939
  • 1
  • 12
  • 22