Conditional Annotations in Spring Boot Conditional Annotations in Spring Boot

Page content

In this tutorial, we’ll take a look at Conditional Annotations in Spring Boot with examples.

Overview

Spring Boot is opinionated. Spring Boot provides default (auto) configuration for a module when it finds related dependency in the classpath.

For Example, Spring Boot provides:-

  1. default embedded Tomcat server when it doesn’t see any server dependency or configuration in the classpath. You can change the default embedded tomcat server to Jetty or Undertow by just changing the dependency in the classpath.
  2. default HttpClient configuration when it find spring-cloud-starter-openfeign dependency in the classpath. You can change the default client to OkHttpClient or ApacheHttpClient by just changing the dependency in the classpath.
  3. default Jackson request and response mapping when it find spring-boot-starter-web dependency in the classpath
  4. default DataSource configuration when it find spring-boot-starter-data-jpa dependency in the classpath

How does Spring Boot make this happen?

Spring Boot does this magic using @Conditional annotation. Spring Boot heavily use @Conditional annotation to load default configurations and beans based on conditions.

These conditions can be anything like:-

  • availability of dependency, resource, or class in the classpath
  • property defined in application.yml or application.properties file, system property, environment variable
  • java version, operating system, cloud platform, web application etc.

How it is useful for us?

Similar to how Spring Boot magically loads default configuration, we also sometime want to load beans and modules into Spring application context based on custom conditions. We can now define and apply these custom conditions using @Conditional annotation.

@Conditional

@Conditional annotation indicate that a component is only eligible for registration in spring context, when all the specified conditions are matched.

The @Conditional annotation may be used in any of the following three ways:


1. As a Method Level Annotation

We can use @Conditional on any method annotated with @Bean annotation to load that bean in Spring context if the condition is met.

@Configuration
class ConditionalBeanConfiguration {

  @Bean
  @Conditional(CustomCondition.class) //<- method level condition
  ConditionalBean conditionalBean(){
    return new ConditionalBean();
  };
}

2. As a Type (Class) Level Annotation

We can use @Conditional on any class annotated with @Component, @Service, @Repository, @Controller, @RestController, or @Configuration annotation to load that class bean in Spring context if the condition is met.

@Component
@Conditional(CustomCondition.class) //<- class level condition
class ConditionalComponent {
}

@Configuration with @Conditional

If a @Configuration class is annotated with @Conditional, all the @Bean methods, @Import annotations, and @ComponentScan annotations associated with that class will be loaded only when the class level condition is met.

@Configuration
@Conditional(CustomCondition.class) //<- class level condition
class ConditionalConfiguration {
  
  @Bean //<- will be loaded only if class level condition is met
  Bean bean(){
    // Code for bean definition
  };
  
}

3. As a Meta Annotation

We can use @Conditional as a meta annotation to create custom conditional annotations.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(CustomCondition.class) //<- meta annotation condition
public @interface CustomConditionAnnotation {
}

Now we can use our custom @CustomConditionAnnotation annotation at method and class level instead of @Conditional annotation like this:-

@Configuration
class ConditionalBeanConfiguration {

  @Bean
  @CustomConditionAnnotation  //<- custom annotation at method level
  ConditionalBean conditionalBean(){
    return new ConditionalBean();
  };
}
@Component
@CustomConditionAnnotation //<- custom annotation at class level
class ConditionalComponent {
}

Custom Condition

You see that in previous examples, we have used CustomCondition.class in @Conditional annotation which provides the logic for condition matching.

Let’s create this custom condition by implementing Spring’s Condition class and provide the matching logic in matches method:-

public class CustomCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("custom.condition.enabled", Boolean.class, false);
    }
}
application.yml
custom.condition.enabled: true

We see that CustomCondition matches when property custom.condition.enabled is set to true.


Combine Conditions with Any Match

We can combine multiple conditions such that it is matched if any one of the underlying condition is matched. In another words, combining the conditions with “OR” logical operator.

Create one more condition to combine

We have already created one custom condition i.e. CustomCondition. Let’s quickly create one more custom condition i.e. AnotherCustomCondition:-

public class AnotherCustomCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("another-custom.condition.enabled", Boolean.class, false);
    }
}
application.yml
another-custom.condition.enabled: true

We see that AnotherCustomCondition matches when property another-custom.condition.enabled is set to true.


AnyNestedCondition

Now let’s combine these two conditions CustomCondition and AnotherCustomCondition with any match by extending Spring’s AnyNestedCondition class:-

public class CombinedConditionsWithAnyMatch extends AnyNestedCondition {

    public CombinedConditionsWithAnyMatch() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
        // super(ConfigurationPhase.REGISTER_BEAN);
    }

    @Conditional(CustomCondition.class)
    static class OnCustomCondition {}

    @Conditional(AnotherCustomCondition.class)
    static class OnAnotherCustomCondition {}
}

ConfigurationPhase

Check the ConfigurationPhase parameter passed into constructor. If you want to apply your combined condition to @Configuration class, use the value PARSE_CONFIGURATION. If you want to apply the condition to @Bean bean, use REGISTER_BEAN as shown in the example above. Spring Boot needs to make this distinction so it can apply the conditions at the right time during application context startup.


Apply Condition with Any Match

Let’s apply this combined condition to a configuration class:-

@Configuration
@Conditional(CombinedConditionsWithAnyMatch.class)
class ConditionalConfiguration {
  
  @Bean
  Bean bean(){
    // TODO
  };
  
}

This configuration will be loaded in Spring application context when any of the condition is true, means when any of the property is set to true:-

application.yml
custom.condition.enabled: true another-custom.condition.enabled: false

Combine Conditions with All Match

We can combine multiple conditions such that it is matched only when all the conditions are matched. In another words, combining the conditions with “AND” logical operator.

AllNestedConditions

This time we combine the two conditions CustomCondition and AnotherCustomCondition with all match by extending Spring’s AllNestedConditions class:-

public class CombinedConditionsWithAllMatch extends AllNestedConditions {

    public CombinedConditionsWithAllMatch() {
        // super(ConfigurationPhase.PARSE_CONFIGURATION);
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @Conditional(CustomCondition.class)
    static class OnCustomCondition {}

    @Conditional(AnotherCustomCondition.class)
    static class OnAnotherCustomCondition {}
}

Apply Condition with All Match

This time we passed ConfigurationPhase.REGISTER_BEAN into constructor as we want to apply the combined condition on @Bean bean like this:-

@Configuration
class ConditionalBeanConfiguration {

  @Bean
  @Conditional(CombinedConditionsWithAllMatch.class) //<- as method level annotation
  ConditionalBean conditionalBean(){
    return new ConditionalBean();
  };
}

This bean will be loaded in Spring application context only when all of the conditions are true, means when all of the properties are set to true:-

application.yml
custom.condition.enabled: true another-custom.condition.enabled: true

Combine Conditions with None Match

We can combine multiple conditions such that it is matched only when all the conditions are NOT matched by extending Spring’s NoneNestedCondition class:-

public class CombinedConditionsWithNoneMatch extends NoneNestedConditions {

    public CombinedConditionsWithNoneMatch() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
        // or super(ConfigurationPhase.REGISTER_BEAN);
    }

    @Conditional(CustomCondition.class)
    static class OnCustomCondition {}

    @Conditional(AnotherCustomCondition.class)
    static class OnAnotherCustomCondition {}
}

The combined condition will be matched with following properties:-

application.yml
custom.condition.enabled: false another-custom.condition.enabled: false

Predefined Conditional Annotations

Spring Boot provides a set of predefined @ConditionalOn... annotations which are very handy. Let’s have a look at them:-

@ConditionalOnProperty

The @ConditionalOnProperty annotation is the most commonly used conditional annotation in Spring Boot. It allows to load classes or beans conditionally depending on a certain property:

@Configuration
@ConditionalOnProperty(
    value="api.doc.enabled", 
    havingValue = "true", 
    matchIfMissing = true)
class ApiDocConfig {
  // TODO
}

The ApiDocConfig is only loaded if the api.doc.enabled property is set to true. If the property is not set at all, it will still be loaded, because we have defined matchIfMissing = true. This way, we have created a config that is loaded by default until we set the property to false.

A common use case is when you want to enable certain config in development environment but disable in production environment or vice versa:-

application-dev.yml
api.doc.enabled: true
application-prod.yml
api.doc.enabled: false

Another common use case is when you want to define a set of configuration files in a common project and use those configurations across many project. You can enable/disable them in specific project by setting the properties:-

Project A -> application.yml
api.doc.enabled: true
Project B -> application.yml
api.doc.enabled: false

@ConditionalOnExpression

If we have a more complex condition based on multiple properties, we can use @ConditionalOnExpression:-

@Configuration
@ConditionalOnExpression(
  "${api.doc.enabled:true} and '${spring.profile.active}'.equalsIgnoreCase('DEV')"
)
class ApiDocConfig {
  // TODO
}

The ApiDocConfig is only loaded if property api.doc.enabled is set to true and spring.profile.active is equal to dev. By appending :true to the api.doc.enabled property, we tell Spring Boot to use true as a default value in the case the properties have not been set.

We can make full use of the Spring Expression Language in the expression.


@ConditionalOnBean

We might want to load a bean only if dependent bean is available in the application context:

@Service
@ConditionalOnBean(ApiDocConfig.class)
class ApiDocService {
    // TODO
}

The ApiDocService is only loaded if there is a bean of class ApiDocConfig in the application context. This way we can define the dependencies of a bean on other beans.


@ConditionalOnMissingBean

Similarly, we can use @ConditionalOnMissingBean if we want to load a bean only if another bean doesn’t exist in the application context:

@Configuration
class DatabaseConfig {

  @Bean
  @ConditionalOnMissingBean
  DataSource dataSource() {
    return new InMemoryDataSource();
  }
}

This examples loads the InMemoryDataSource into the application context if there is no other DataSource exist in application context. This is very similar to what Spring Boot does internally to provide an in-memory database in a test context.


@ConditionalOnResource

The @ConditionalOnResource is used when we want to load a bean depending upon the certain resource availability in the class path

@Configuration
@ConditionalOnResource(resources = "/logback.xml")
class LoggingConfig {
  // TODO
}

The LoggingConfig is only loaded when the logback.xml configuration file is available in the classpath. This way, we can create similar config classes that are only loaded if their respective configuration file is available.



The conditional annotations described above are the most common ones which we generally use in our Spring Boot Project. Spring Boot provides many other conditional annotations. They are, however, not as common and more suited for framework development rather than application development (Spring Boot uses some of them heavily behind the scene). So, let’s only have a brief look at them here.


@ConditionalOnClass

Load a bean only when the specified class is in the classpath. We can specify the class either by fully qualified name or value.

@Service
@ConditionalOnClass(name = "com.example.config.LoggingConfig")
class LoggingService {
  // Code runs when class in available in classpath
}

@Service
@ConditionalOnClass(LoggingConfig.class)
class LoggingService {
  // Code runs when class in available in classpath
}

Similarly we can use @ConditionalOnMissingClass annotation to load a bean only if specified class in NOT in the classpath.


@ConditionalOnWebApplication

Load a bean only when application is a web application. By default, any web application will match but it can be narrowed using the type attribute.

@Configuration
@ConditionalOnWebApplication
class RunsOnAnyWebApplication {
  // Code runs on web application
}

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
class RunsOnServletBasedWebApplication {
  // Code runs on Servlet based web application
}

Similarly we can use @ConditionalOnNotWebApplication annotation to load a bean only when the application context is a NOT a web application context.


@ConditionalOnJava

Load a bean only when application is running on a specified JVM version. By default, version equal and above will match but it can be changed using the range attribute

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class RunsOnJavaEightAndAbove {
  // Code runs on Java-8 and above versions
}

@Configuration
@ConditionalOnJava(value = JavaVersion.ELEVEN, range = ConditionalOnJava.Range.OLDER_THAN)
class RunsOnBelowJavaEleven {
  // Code runs below Java-11. 
  // Code doesn't run on Java-11 and above versions
}

@ConditionalOnCloudPlatform

Load a bean only when application is running on a specified cloud platform:

@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
class RunsOnKubernetesCloudPlatform {
  // Code runs on application running on Kubernetes
}

@ConditionalOnWarDeployment

Load a bean only when the application is a traditional war packaging and deployment. This condition returns false for applications running with embedded servers.

@Configuration
@ConditionalOnWarDeployment
class RunsWithWarPackages {
  // Code runs with WAR package deployment
}

@ConditionalOnJndi

Load a bean only when a specified JNDI location exist. If no locations are specific the condition matches solely based on the presence of an javax.naming.InitialContext.

@Configuration
@ConditionalOnJndi("java:comp/env/ejb/myEJB")
class RunsWithJndiLocationAvailability {
  // Code runs when JNDI location is available
}

@ConditionalOnSingleCandidate

Similar to @ConditionalOnBean, but load a bean only when a single candidate for the specified bean class can be determined. The condition will also match if multiple matching bean instances are already contained in the BeanFactory but a primary @Primary candidate has been defined; essentially, the condition match if auto-wiring a bean with the defined type will succeed. It is strongly recommended to use this condition on auto-configuration classes only.

@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class RunsWithSingleDataSourceBean {
  // Code runs when single data source bean is determined
}

@ConditionalOnManagementPort

Load a bean based on management server port management.server.port conditions, when:-

  • the port is disabled means it is not defined
  • the port is same or different from server port server.port
@Configuration
@ConditionalOnManagementPort(ManagementPortType.DISABLED)
class ManagementPortIsDisabled {
  // Code runs when management port is disabled
}

@Configuration
@ConditionalOnManagementPort(ManagementPortType.SAME)
class ManagementPortIsSameAsServerPort {
  // Code runs when management port is same as server port
}

@Configuration
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
class ManagementPortIsDifferentFromServerPort {
  // Code runs when management port is different from server port
}

@ConditionalOnAvailableEndpoint

Load a bean when management endpoint is available. An endpoint is considered available if it is both enabled and exposed using management.endpoints.web.exposure.include

@Configuration
@ConditionalOnAvailableEndpoint(endpoint = InfoEndpoint.class)
class InfoEndpointIsAvailable {
  // Code runs when info management endpoint in enabled and exposed
}

The endpoint should be a bean of either an @Endpoint or an @EndpointExtension. Info endpoint is provided by Spring Boot out of the box.

Custom Management Endpoint

You can create custom management endpoint like this:-

@Component
@Endpoint(id = "custom-endpoint")
public class CustomEndpoint {

    @ReadOperation
    public String print() {
        return "This is custom management endpoint";
    }
}

Now let’s load a configuration only when our custom endpoint is available:-

@Configuration
@ConditionalOnAvailableEndpoint(endpoint = CustomEndpoint.class)
class CustomEndpointConfiguration {
  // Configuration for custom endpoint
}

@ConditionalOnEnabledHealthIndicator

Load a bean when health indicator is enabled from property management.health.<name>.enabled where <name> is the value specified.

@Configuration
@ConditionalOnEnabledHealthIndicator(value = "heartbeat")
class HeatbeatHealthIndicator {
  // Code runs when management.health.heartbeat.enabled property is set to true.
}

Conclusion

The Conditional annotation gives more power to Spring Boot to provide opinionated configuration and also gives us flexibility to load beans based on custom conditions using @Conditional and very handy predefined @ConditionalOn... annotations.

We can also combine the custom conditions using AllNestedConditions, AnyNestedCondition, or NoneNestedCondition. These helps us to modularize code based on environment and other conditions.

We should use the Conditional annotations wisely as they are difficult to debug and manage when overused.

The source code for examples in this article is available on github/springboot-config