Can I negate (!) a collection of spring profiles?

18,876

Solution 1

Since Spring 5.1 (incorporated in Spring Boot 2.1) it is possible to use a profile expression inside profile string annotation (see the description in Profile.of(..) for details).

So to exclude your bean from certain profiles you can use an expression like this:

@Profile("!dev & !prof1 & !prof2")

Other logical operators can be used as well, for example:

@Profile("test | local")

Solution 2

Short answer is: You can't in versions of Spring prior to Spring 5.1 (i.e. versions of Spring Boot prior to 2.1).

But there is a neat workarounds that exists thanks to the @Conditional annotation.

Create Condition matchers:

public static abstract class ProfileCondition extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        if (matchProfiles(conditionContext.getEnvironment())) {
            return ConditionOutcome.match("A local profile has been found.");
        }
        return ConditionOutcome.noMatch("No local profiles found.");
    }

    protected static abstract boolean matchProfiles(final Environment environment);
}

public class DevProfileCondition extends ProfileCondition {
   protected boolean matchProfiles(final Environment environment) {    
        return Arrays.stream(environment.getActiveProfiles()).anyMatch(prof -> {
            return prof.equals("dev") || prof.equals("prof1") || prof.equals("prof2");
        });
    }
}

public static class ProdProfileCondition extends ProfileCondition {
   protected boolean matchProfiles(final Environment environment) {    
        return Arrays.stream(environment.getActiveProfiles()).anyMatch(prof -> {
            return (!prof.equals("dev") && !prof.equals("prof1") && !prof.equals("prof2"));
        });
    }
}

Use it

@Conditional(value = {DevProfileCondition.class})
public class MockImpl implements MyInterface {...}

@Conditional(value = {ProdProfileCondition.class})
public class RealImp implements MyInterface {...}

However, this aproach requires Springboot.

Solution 3

From what I understand, what you want to do is be capable of replacing some of your beans with some stub/mock beans for specific profiles. There are 2 ways to address this:

  • Exclude the not needed beans for the corresponding profiles and include by default everything else
  • Include only the required beans for each profile

The first option is feasible but difficult. This is because the default behaviour of Spring when providing multiple profiles in @Profile annotation is an OR condition (not an AND as you would need in your case). This behaviour of Spring is the more intuitive, because ideally each profile should correspond to each configuration of your application (production, unit testing, integration testing etc.), so only one profile should be active at each time. This is the reason OR makes more sense than AND between profiles. As a result of this, you can work around this limitation, probably by nesting profiles, but you would make your configuration very complex and less maintainable.

Thus, I suggest you go with the second approach. Have a single profile for each configuration of your application. All the beans that are the same for every configuration can reside in a class that will have no @Profile specified. As a result, these beans will be instantiated by all the profiles. For the remaining beans that should be distinct for each different configuration, you should create a separate @Configuration class (for each Spring profile), having all of them with the @Profile set to the corresponding profile. This way, it will be really easy to tract what is injected in every case.

This should be like below:

@Profile("dev")
public class MockImp implements MyInterface {...}

@Profile("prof1")
public class MockImp implements MyInterface {...}

@Profile("prof2")
public class MockImp implements MyInterface {...}

@Profile("the-last-profile") //you should define an additional profile, not rely on excluding as described before
public class RealImp implements MyInterface {...}

Last, @Primary annotation is used to override an existing beans. When there are 2 beans with the same type, if there is no @Primary annotation, you will get an instantiation error from Spring. If you define a @Primary annotation for one of the beans, there will be no error and this bean will be injected everywhere this type is required (the other one will be ignored). As you see, this is only useful if you have a single Profile. Otherwise, this will also become complicated as the first choice.

TL;DR: Yes you can. For each type, define one bean for each profile and add a @Profile annotation with only this profile.

Share:
18,876
HellishHeat
Author by

HellishHeat

Full stack developer with 10+ years experience in Financial services, Pharma and Energy.

Updated on June 03, 2022

Comments

  • HellishHeat
    HellishHeat almost 2 years

    Is it possible to configure a bean in such a way that it wont be used by a group of profiles? Currently I can do this (I believe):

    @Profile("!dev, !qa, !local")
    

    Is there a neater notation to achieve this? Let's assume I have lots of profiles. Also, if I have a Mock and concrete implementation of some service (or whatever), Can I just annotate one of them, and assume the other will be used in all other cases? In other words, is this, for example, necessary:

    @Profile("dev, prof1, prof2")
    public class MockImp implements MyInterface {...}
    
    @Profile("!dev, !prof1, !prof2") //assume for argument sake that there are many other profiles
    public class RealImp implements MyInterface {...}
    

    Could I just annotate one of them, and stick a @Primary annotation on the other instead?

    In essence I want this:

    @Profile("!(dev, prof1, prof2)")
    

    Thanks in advance!

  • HellishHeat
    HellishHeat over 4 years
    Thanks for all the info. Particularly the OR vs AND situation. It is not realistic or desirable for me to create duplicate Mock or Real implementations, one for each profile. I was hoping for something that would achieve this: @Profile("!(dev, prof1, prof2)"). This is not possible, right? –
  • HellishHeat
    HellishHeat almost 4 years
    Thanks for the comment, It's admirable to update old posts with fresh up-to-date info.