77

In my Spring-Boot-App I want to conditionally declare a Bean, depending on (un)loaded spring-profiles.

The conditon:

Profile "a" NOT loaded  
AND  
Profile "b" NOT loaded 

My solution so far (which works):

@Bean
@ConditionalOnExpression("#{!environment.getProperty('spring.profiles.active').contains('a') && !environment.getProperty('spring.profiles.active').contains('b')}")
    public MyBean myBean(){/*...*/}

Is there a more elegant (and shorter) way to explain this condition?
Especially I want to get rid of the usage of Spring Expression Language here.

Mike Boddin
  • 1,241
  • 2
  • 12
  • 22

5 Answers5

137

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

In Spring 5.1.4 (Spring Boot 2.1.2) and above it is as easy as:

@Component
@Profile("!a & !b")
public class MyComponent {}

In Spring 4.x and 5.0.x:

There are many approaches for this Spring versions, each one of them has its pro's and con's. When there aren't many combinations to cover I personally like @Stanislav answer with the @Conditional annotation.

Other approaches can be found in this similar questions:

Spring Profile - How to include AND condition for adding 2 profiles?

Spring: How to do AND in Profiles?

icyerasor
  • 4,973
  • 1
  • 43
  • 52
f-CJ
  • 4,235
  • 2
  • 30
  • 28
  • 1
    For this specific case (double negation), you need to make extra sure, that it's spring-core: 5.1.4+ / spring-boot: 2.1.2+, as there was a nasty bug up until those versions: https://github.com/spring-projects/spring-framework/issues/22138 – icyerasor Feb 20 '20 at 17:31
  • Can this have more than two profiles? I believe that should work correctly. For e.x.: "!a & !b & !c" – vermachint Jun 14 '22 at 11:58
37

If you have a single profile you could simply use a @Profile annotation with the not operator. It also accepts multiple profiles, but with the OR condition.

So, the alternative solution is to use a custom Condition with the @Conditional annotation. Like so:

public class SomeCustomCondition implements Condition {
  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

    // Return true if NOT "a" AND NOT "b"
    return !context.getEnvironment().acceptsProfiles("a") 
                  && !context.getEnvironment().acceptsProfiles("b");
  }
}

And then annotate your method with it, like:

@Bean
@Conditional(SomeCustomCondition.class)
public MyBean myBean(){/*...*/}
Rylander
  • 19,449
  • 25
  • 93
  • 144
Stanislav
  • 27,441
  • 9
  • 87
  • 82
  • 1
    Thank you, I like this approach. +1 for using `Condition`. I will just wait some time before I accept this answer. Maybe there comes another suggestion which is even more elegant ;-) (But I don't think so) – Mike Boddin Feb 16 '16 at 13:13
  • 1
    I tried same above but for some reason it's ignoring @Conditional annotation. Any suggestion? – Mind Peace Aug 09 '17 at 19:44
  • acceptProfiles () is a vararg. We can specify multiple profiles at once like return !context.getEnvironment().acceptsProfiles("a", "b") ; – Ashutosh Srivastav Jun 21 '21 at 15:52
21

I prefer this solution which is more verbose but still fine for two profiles only:

@Profile("!a")
@Configuration
public class NoAConfig {

    @Profile("!b")
    @Configuration
    public static class NoBConfig {
        @Bean
        public MyBean myBean(){
            return new MyBean();
        }
    }

}
willerr
  • 266
  • 2
  • 4
17

Unfortunately I don't have a shorter solution for you, but if it is suitable in your case to create the same beans for each profile, you may consider the following approach.

@Configuration
public class MyBeanConfiguration {

   @Bean
   @Profile("a")
   public MyBean myBeanForA() {/*...*/}

   @Bean
   @Profile("b")
   public MyBean myBeanForB() {/*...*/}

   @Bean
   @ConditionalOnMissingBean(MyBean.class)
   public MyBean myBeanForOthers() {/*...*/}

}
dmytro
  • 1,141
  • 11
  • 13
  • Thank you, but this is not suitable for me. Let's say we have Profile `a` AND `b` loaded, then the `MyBean` is loaded twice in this case and usually we don't want this behaviour. The second thing is, that this is not a solution for my question. When both profiles are not loaded then the `MyBean` should not be declared, too. – Mike Boddin Feb 16 '16 at 13:09
  • 5
    Using `@ConditionalOnMissingBean` like this is dangerous. To be safe, it should only be used on auto-configuration classes – Andy Wilkinson Feb 16 '16 at 13:18
  • Thanks for your comments, guys. I used to using profiles as bunches of environment-related configuration properties, like 'dev', 'test', etc. Really, this approach cannot be used in case both profiles are loaded. – dmytro Feb 18 '16 at 14:34
  • @AndyWilkinson could you please to explain why this approach is unsafe ? – wakedeer Jan 06 '23 at 17:55
  • @wakedeer Without the ordering guarantees that auto-configuration provides, the condition on `myBeanForOthers` may be evaluated before some other `@Bean` method that also defines a bean of type `MyBean`. This would leave you with two beans of type `MyBean`. – Andy Wilkinson Jan 09 '23 at 12:37
3

A variant of @Stanislav answer which is a tiny bit more readable, for Spring versions up to 5.0.x / Spring Boot 2.0.x.

public class SomeCustomCondition implements Condition {
  @Override
  public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
    final Environment environment = context.getEnvironment();

    // true if profile is NOT "a" AND NOT "b" AND NOT "c"
    return !environment.acceptsProfiles("a", "b", "c");
  }
}

For newer versions of Spring / Spring Boot, see the f-CJ answer.

t0r0X
  • 4,212
  • 1
  • 38
  • 34