Spring Boot: Conditional Beans

{getToc} $title={Table of Contents}

Conditional Beans with Spring Boot


Introduction

Spring has introduced the @Conditional annotation that allows us to define custom conditions to apply to parts of our application context. Spring Boot builds on top of that and provides some pre-defined conditions so we don’t have to implement them ourselves.

In this tutorial, we’ll have a look some use cases that explain why we would need conditionally loaded beans at all. Then, we’ll see how to apply conditions and which conditions Spring Boot offers. To round things up, we’ll also implement a custom condition.


Why do we need Conditional Beans?

A Spring application context contains an object graph that makes up all the beans that our application needs at runtime. Spring’s @Conditional annotation allows us to define conditions under which a certain bean is included into that object graph.

Why would we need to include or exclude beans under certain conditions?

In my experience, the most common use case is that certain beans don’t work in a test environment. They might require a connection to a remote system or an application server that is not available during tests. So, we want to modularize our tests to exclude or replace these beans during tests.

Another use case is that we want to enable or disable a certain cross-cutting concern. Imagine that we have built a module that configures security. During developer tests, we don’t want to type in our usernames and passwords every time, so we flip a switch and disable the whole security module for local tests.

Also, we might want to load certain beans only if some external resource is available without which they cannot work. For instance, we want to configure our Logback logger only if a logback.xml file has been found on the classpath.

We’ll see some more use cases in the discussion below.


Declaring Conditional Beans

Anywhere we define a Spring bean, we can optionally add a condition. Only if this condition is satisfied will the bean be added to the application context. To declare a condition, we can use any of the @Conditional... annotations that are described below.

But first, let’s look at how to apply a condition to a certain Spring bean.


Conditional @Bean

If we add a condition to a single @Bean definition, this bean is only loaded if the condition is met:

@Configuration
class ConditionalBeanConfiguration {

  @Bean
  @Conditional... // <--
  ConditionalBean conditionalBean(){
    return new ConditionalBean();
  };
}


Conditional @Configuration

If we add a condition to a Spring @Configuration, all beans contained within this configuration will only be loaded if the condition is met:

@Configuration
@Conditional... // <--
class ConditionalConfiguration {
  
  @Bean
  Bean bean(){
    ...
  }; 
}


Conditional @Component

Finally, we can add a condition to any bean declared with one of the stereotype annotations @Component, @Service, @Repository, or @Controller:

@Component
@Conditional... // <--
class ConditionalComponent {
}


Pre-Defined Conditions

Spring Boot offers some pre-defined @ConditionalOn... annotations that we can use out-of-the box. Let’s have a look at each one in turn.


@ConditionalOnProperty

The @ConditionalOnProperty annotation is, in my experience, the most commonly used conditional annotation in Spring Boot projects. It allows to load beans conditionally depending on a certain environment property:

@Configuration
@ConditionalOnProperty(
    value="module.enabled", 
    havingValue = "true", 
    matchIfMissing = true)
class CrossCuttingConcernModule {
  ...
}

The CrossCuttingConcernModule is only loaded if the module.enabled property has the value true. If the property is not set at all, it will still be loaded, because we have defined matchIfMissing as true. This way, we have created a module that is loaded by default until we decide otherwise.

In the same way we might create other modules for cross-cutting concerns like security or scheduling that we might want to disable in a certain (test) environment.



@ConditionalOnExpression

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

@Configuration
@ConditionalOnExpression(
    "${module.enabled:true} and ${module.submodule.enabled:true}"
)
class SubModule {
  ...
}


The SubModule is only loaded if both properties module.enabled and module.submodule.enabled have the value true. By appending :true to the properties we tell Spring to use true as a default value in the case the properties have not been set. We can use the full extend of the Spring Expression Language.

This way we can, for instance, create sub modules that should be disabled if the parent module is disabled, but can also be disabled if the parent module is enabled.



@ConditionalOnBean

Sometimes, we might want to load a bean only if a certain other bean is available in the application context:

@Configuration
@ConditionalOnBean(OtherModule.class)
class DependantModule {
  ...
}

The DependantModule is only loaded if there is a bean of class OtherModule in the application context. We could also define the bean name instead of the bean class.

This way, we can define dependencies between certain modules, for example. One module is only loaded if a certain bean of another module is available.



@ConditionalOnMissingBean

Similarly, we can use @ConditionalOnMissingBean if we want to load a bean only if a certain other bean is not in the application context:

@Configuration
class OnMissingBeanModule {

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

In this example, we’re only injecting an in-memory datasource into the application context if there is not already a datasource available. This is very similar to what Spring Boot does internally to provide an in-memory database in a test context.


@ConditionalOnResource

If we want to load a bean depending on the fact that a certain resource is available on the class path, we can use @ConditionalOnResource:

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

The LogbackModule is only loaded if the logback configuration file was found on the classpath. This way, we might create similar modules that are only loaded if their respective configuration file has been found.



Other Conditions

The conditional annotations described above are the more common ones that we might use in any Spring Boot application. Spring Boot provides even more conditional annotations. They are, however, not as common and some are more suited for framework development rather than application development (Spring Boot uses some of them heavily under the covers). So, let’s only have a brief look at them here.

@ConditionalOnClass


Load a bean only if a certain class is on the classpath:

@Configuration
@ConditionalOnClass(name = "this.clazz.does.not.Exist")
class OnClassModule {
  ...
}

@ConditionalOnMissingClass 

Load a bean only if a certain class is not on the classpath:

@Configuration
@ConditionalOnMissingClass(value = "this.clazz.does.not.Exist")
class OnMissingClassModule {
  ...
}

@ConditionalOnJndi


Load a bean only if a certain resource is available via JNDI:

@Configuration
@ConditionalOnJndi("java:comp/env/foo")
class OnJndiModule {
  ...
}

@ConditionalOnJava


Load a bean only if running a certain version of Java:

@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
class OnJavaModule {
  ...
}


@ConditionalOnSingleCandidate


Similar to @ConditionalOnBean, but will only load a bean if a single candidate for the given bean class has been determined. There probably isn’t a use case outside of auto-configurations:

@Configuration
@ConditionalOnSingleCandidate(DataSource.class)
class OnSingleCandidateModule {
  ...
}


@ConditionalOnWebApplication


Load a bean only if we’re running inside a web application:

@Configuration
@ConditionalOnWebApplication
class OnWebApplicationModule {
  ...
}


@ConditionalOnNotWebApplication


Load a bean only if we’re not running inside a web application:

@Configuration
@ConditionalOnNotWebApplication
class OnNotWebApplicationModule {
  ...
}


@ConditionalOnCloudPlatform


Load a bean only if we’re running on a certain cloud platform:

@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
class OnCloudPlatformModule {
  ...
}


Custom Conditions

Aside from the conditional annotations, we can create our own and combine multiple conditions with logical operators.



Defining a Custom Condition

Imagine we have some Spring beans that talk to the operating system natively. These beans should only be loaded if we’re running the application on the respective operating system.

Let’s implement a condition that loads beans only if we’re running the code on a unix machine. For this, we implement Spring’s Condition interface:

class OnUnixCondition implements Condition {

  @Override
    public boolean matches(
        ConditionContext context, 
        AnnotatedTypeMetadata metadata) {
  	  return SystemUtils.IS_OS_LINUX;
    }
}


We simply use Apache Commons' SystemUtils class to determine if we’re running on a unix-like system. If needed, we could include more sophisticated logic that uses information about the current application context (ConditionContext) or about the annotated class (AnnotatedTypeMetadata).

The condition is now ready to be used in combination with Spring’s @Conditional annotation:

@Bean
@Conditional(OnUnixCondition.class)
UnixBean unixBean() {
  return new UnixBean();
}


Combining Conditions with OR

If we want to combine multiple conditions into a single condition with the logical “OR” operator, we can extend AnyNestedCondition:

class OnWindowsOrUnixCondition extends AnyNestedCondition {

  OnWindowsOrUnixCondition() {
    super(ConfigurationPhase.REGISTER_BEAN);
  }

  @Conditional(OnWindowsCondition.class)
  static class OnWindows {}

  @Conditional(OnUnixCondition.class)
  static class OnUnix {}

}


Here, we have created a condition that is satisfied if the application runs on windows or unix.

The AnyNestedCondition parent class will evaluate the @Conditional annotations on the methods and combine them using the OR operator.

We can use this condition just like any other condition:

@Bean
@Conditional(OnWindowsOrUnixCondition.class)
WindowsOrUnixBean windowsOrUnixBean() {
  return new WindowsOrUnixBean();
}


Is your AnyNestedCondition or AllNestedConditions not working?
Check the ConfigurationPhase parameter passed into super(). If you want to apply your combined condition to @Configuration beans, use the value PARSE_CONFIGURATION. If you want to apply the condition to simple beans, 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.


Combining Conditions with AND

If we want to combine conditions with “AND” logic, we can simply use multiple @Conditional... annotations on a single bean. They will automatically be combined with the logical “AND” operator so that if at least one condition fails, the bean will not be loaded: 

@Bean
@ConditionalOnUnix
@Conditional(OnWindowsCondition.class)
WindowsAndUnixBean windowsAndUnixBean() {
  return new WindowsAndUnixBean();
}


This bean should never load, unless someone has created a Windows / Unix hybrid that I’m not aware of.

Note that the @Conditional annotation cannot be used more than once on a single method or class. So, if we want to combine multiple annotations this way, we have to use custom @ConditionalOn... annotations, which do not have this restriction. Below, we’ll explore how to create the @ConditionalOnUnix annotation.

Alternatively, if we want to combine conditions with AND into a single @Conditional annotation, we can extend Spring Boot’s AllNestedConditions class which works exactly the same as AnyNestedConditions described above.


Combining Conditions with NOT

Similar to AnyNestedCondition and AllNestedConditions, we can extend NoneNestedCondition to only load beans if NONE of the combined conditions match.


Defining a Custom @ConditionalOn… Annotation

We can create a custom annotation for any condition. We simply need to meta-annotate this annotation with @Conditional:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLinuxCondition.class)
public @interface ConditionalOnUnix {}


Spring will evaluate this meta annotation when we annotate a bean with our new annotation:

@Bean
@ConditionalOnUnix
LinuxBean linuxBean(){
  return new LinuxBean();
}


Conclusion

With the @Conditional annotation and the possibility to create custom @Conditional... annotations, Spring already gives us a lot of power to control the content of our application context.

Spring Boot builds on top of that by bringing some convenient @ConditionalOn... annotations to the table and by allowing us to combine conditions using AllNestedConditions, AnyNestedCondition or NoneNestedCondition.



Thanks to the original article and a writer :

https://reflectoring.io/spring-boot-conditionals/

This article is accompanied by a working code example on GitHub.



Previous Post Next Post