Spring is a powerful framework which serves billion of requests worldwide every minute. One of the things that made it special was its Dependency injection capabilities. At the start of the application Spring scans for the classes in the classpath, identifies the configuration classes as well as the classes containing bean related annotations. Conditionals have a pivotal role to the environment creation.👁 Image
There are many reasons why you need to use Conditionals.
Overall think about your modular application. Your application has to operate on various different environments. You got Development, Staging, UAT, Production etc. Your code should be as close to production as it can be, yet there can always be some variations.
What if your application has to work on multiple clouds? In that case a broker class might need different implementations. For example on AWS you want to dispatch messages using SQS, on Azure you shall do so using storage Queues.
Usually we do this by creating an interface specifying the functionality we want (plain old strategy pattern)
public interface MessagePublisher {
void publish(String message);
}
SQS implementation
public class SqsMessagePublisher implements MessagePublisher {
public void publish(String message) {
...
}
}
Azure implementation
public class AzureStorageQueuePublisher implements MessagePublisher {
public void publish(String message) {
...
}
}
You could define the implementation to use with a @Configuration bean that checks the defined properties.
@Configuration
public class MessagePublisherConfig {
@Value("${message.publisher.type:sqs}")
private String publisherType;
@Bean
public MessagePublisher messagePublisher() {
if (publisherType != null && publisherType.equalsIgnoreCase("azure")) {
return new AzureStorageQueuePublisher();
} else {
// Default to SQS or handle other cases
return new SqsMessagePublisher();
}
}
}
This code works however thanks to Spring, there is no need for that if statement.
Conditional on Property
Spring has this problem already shorted with conditional beans.
We shall change our classes by using he ConditionalOnProperty annotation.
@Component
@ConditionalOnProperty(name = "message.publisher.type", havingValue = "azure")
public class AzureStorageQueuePublisher implements MessagePublisher {
public void publish(String message) {
...
}
}
@Component
@ConditionalOnProperty(name = "message.publisher.type", havingValue = "sqs")
public class SqsMessagePublisher implements MessagePublisher {
public void publish(String message) {
...
}
}
Conditional on class
Now you might think that having both implementations in one jar is kinda bloated. It is likely that a lean jar is better. One jar built with the AWS dependencies and one jar built with the Azure dependencies. Beyond the capabilities of the built tool used (for example profiles on maven) our codebase should be able to handle any class loading issues.
There is a Conditional annotation based on the presence of classes.
@Component
@ConditionalOnClass(QueueServiceClient.class)
public class AzureStorageQueuePublisher implements MessagePublisher {
public void publish(String message) {
}
}
@Component
@ConditionalOnClass(SqsClient.class)
public class SqsMessagePublisher implements MessagePublisher {
public void publish(String message) {
}
}
Behind the scenes spring scans the class definition and identifies if the required class exists on the binary before proceeding on instantiation. The above option enables us to have a jar with less dependencies that will instantiate the right bean implementations based on the environment.
ConditionalOnMissingBean
Regardless of the environment we might want to spin up a default implementation of the MessagePublisher, in case certain criteria are not fulfilled. In that case the ConditionalOnMissingBean annotation can help.
@Component
@ConditionalOnMissingBean(MessagePublisher.class)
public class DefaultPublisher implements MessagePublisher {
@Override
public void publish(String message) {
}
}
Simplify
As we can see Conditionals are powerful, yet they do bring a configuration overhead which can be error prone or cumbersome. Instead of using the same configuration all over again we can simplify it by defining a conditional annotation with the configurations preset.
@ConditionalOnProperty(name = "message.publisher.type", havingValue = "azure")
@Retention(RetentionPolicy.RUNTIME)
public @interface AzurePublisherEnabled {
}
On the above example we can use the AzurePublisherEnabled annotation for the Azure only implementation.
Customization
So far conditionals have a wide range of options and ways to simplify them but what if you want something more complex that the existing annotations cannot fulfil? In that case you can create your own conditions.The condition can be fulfilled based on information retrieved by the ConditionContext whether they are environment variables or other aspects of the running program.
package com.gkatzioura.broker;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class LocalBrokerCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//Logic
return false;
}
}
To use this conditional handler you just have to configure it in the annotation.
@Conditional(LocalBrokerCondition.class)
public class LocalPublisher implements MessagePublisher {
@Override
public void publish(String message) {
}
}
Testing
So far so good, so what about testing? One option is to spin up a spring context using
@SpringBootTest and check if certain bean implementation have been instantiated. Another handy way is to use the ApplicationContextRunner
@Test
public void testShouldBeDisabled() {
ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(UserConfigurations.of(AzureStorageQueuePublisher.class));
runner.withPropertyValues("message.publisher.type=azure")
.run(context -> assertThat(context.getBean(MessagePublisher.class)).isInstanceOf(AzureStorageQueuePublisher.class));
}
This is very elegant and removes the need to create a complex spring environment for unit tests.
So that’s it about conditionals, pretty sure you are gonna stumble on them on most spring based open source projects! Happy hacking 😉