Conditional Beans with Spring Boot
Omar Ismail
Senior Software Engineer @ Digitinary | Java 8 Certified? | Spring & Spring Boot?????? | AWS? | Microservices ?? | RESTFul Apis & Integrations ?? FinTech ?? | Open Banking ?? | Digital Payments and Transformation??
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.
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. These tools allow us to?modularize our production code?as well as?our tests.
With power comes responsibility, however, so we should take care not to litter our application context with conditions, lest we lose track of what is loaded when.
Helping Pro Java Devs with Microservices & AWS | AWS Certified | Book Writer | 16 Yrs in Java |DevOps| Agile | Test Automation| Coordinator @ TDC SP |
9 个月Great content, rich in details, congrats!