Conditional Annotations in Spring Boot
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:-
- 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.
- 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. - default Jackson request and response mapping when it find
spring-boot-starter-web
dependency in the classpath - 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