Using Conditional in Spring

In this article I want to describe a very useful, and often used Conditional annotation and the Condition interface.







Spring context is a huge container of various beans, both spring itself and custom ones. I always want to have flexible management tools for this bin zoo. The @Conditional annotation is precisely created for this.







The most common way to control the spring context is through profiles. They allow you to quickly and easily control the creation of beans. But sometimes more fine-tuning may be required.







For example, during testing, a problem arises: a unit test on a developer's machine requires an X-type bin for its work, when running the same test on an assembly server, a Y-bin is required, and a Z-bin is required on production. @Conditional offers a simple and easy in this case decision. Just as often happens when working in unsynchronized teams, someone does not have time to complete his revision by the deadline, and your functionality is already ready. It is necessary to adapt to these conditions and change the behavior. That is, add the ability to change the application context without recompiling, for example, changing only one parameter in the configuration.







Let's consider this annotation in more detail. Above each bin in the source code, we can add @Conditional and when it is created, the spring will automatically check the conditions specified in this annotation.







In the official documentation, it is declared like this:







@Target(value={TYPE,METHOD}) @Retention(value=RUNTIME) @Documented public @interface Conditional
      
      





At the same time, you need to pass a set of conditions into it:







 Class<? extends Condition>[]
      
      





Where Conditional is the functional interface that contains the method







 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)
      
      





Let's check how this works in practice, using a living example. Our application has interfaces in the form of soap / rest - services, and in the form of JMS. But the administrators did not have time to prepare the appropriate infrastructure in time - we cannot use JMS.

There is some java configuration for JMS in our project:







 @Configuration public class JmsConfig { ... }
      
      





Spring finds this configuration and starts initializing it. Next all other dependent beans are pulled up, for example, reading from the queue. To disable the creation of this configuration, we will use the Conditional derivative annotation - ConditioanalOnProperty







 @ConditionalOnProperty( value="project.mq.enabled", matchIfMissing = false) @Configuration public class JmsConfig { ... }
      
      





Here we inform the context of the builder that we will create this configuration only if there is a positive value of the project.mq.enabled constant in the settings file.

Now let's move on to the dependent beans and mark them with the ConditioanalOnBean annotation, which will prevent the spring from creating beans that are dependent on our configuration.







 @ConditionalOnBean(JmsConfig.class) @Component public class JmsConsumer { ... }
      
      





Thus, using one parameter, we can disable the components of the application that we do not need, and then add them to the context by changing the configuration.







Together with the framework, there are a large number of ready-made annotations covering 99% of the needs of the developer (which will be described later in the article). But what if you need to handle some specific situation. To do this, you can add your own custom logic to the spring.







Suppose we have some bean - SuperDBLogger , which we want to create only if there is an @Loggable annotation over any of our bins . How it will look in the code:







 @Component @ConditionalOnLoggableAnnotation public class SuperDBLogger
      
      





Consider how the @ConditionalOnLoggableAnnotation annotation works :







 @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Conditional(OnLoggableAnnotation.class) public @interface ConditionalOnLoggableAnnotation { }
      
      





We do not need any more parameters, now let's move on to the logic itself - the contents of the OnLoggableAnnotation class. In it, we redefine the matches method, in which we implement the search for marked beans in our package.







 public class OnLoggableAnnotation implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { ClassPathScanner scanner = new ClassPathScanner(); scanner.addIncludeFilter(new AnnotationTypeFilter(Loggable.class)); Set<BeanDefinition> bd = scanner.findInPackage("ru.habr.mybeans"); if (!bd.isEmpty()) return true; return false; } }
      
      





Thus, we created a rule according to which Spring now creates SuperDBLogger . For SpringBoot followers, the creators of the framework have created SpringBootCondition , which is the successor to Condition . It differs in the signature of the redefined method:







 public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
      
      





That is, in addition to the answer, we need a bin or not, we can add a message that can then be seen in the spring logs, realizing why the bin was created or not.







Various conditions can be combined to create more complex conditions; these mechanisms provide the classes AnyNestedCondition, AllNestedConditions and NoneNestedConditions . Suppose we want to create two conditions so that when one of them is executed, our bean is created. To do this, create your own class and inherit it from AnyNestedCondition .







 public class AnnotationAndPropertyCondition extends AnyNestedCondition { public AnnotationAndPropertyCondition() { super(REGISTER_BEAN); } @ConditionalOnProperty(value = "db.superLogger") static class Condition1 {} @ConditionalOnLoggableAnnotation static class Condition2 {} }
      
      





The class does not need to be additionally marked with any annotations; the spring itself will find it and correctly process it. The user only needs to indicate at what stage of the configuration the conditions are met: ConfigurationPhase .REGISTER_BEAN - when creating regular beans, ConfigurationPhase .PARSE_CONFIGURATION - when working with configurations (that is, for bins marked with @Configuration annotation).







Similarly, for the classes AllNestedConditions and NoneNestedConditions - the first monitors the fulfillment of all conditions, the second - that no condition is met.







Also, in order to check several conditions, you can pass several classes with conditions to @Conditional . For example, @Conditional ({OnLoggableAnnotation.class, AnnotationAndPropertyCondition.class}) . Both must return true so that the condition is satisfied and the bean is created.







As I mentioned above, there are already many ready-made solutions with spring, which are presented in the table below.







annotation Description
ConditionalOnBean The condition is met if the desired bean is present in BeanFactory.
ConditionalOnClass The condition is satisfied if the required class is in the classpath.
ConditionalOnCloudPlatform The condition is met when a specific platform is active.
ConditionalOnExpression The condition is true when the SpEL expression returns a positive value.
ConditionalOnJava The condition is met when the application is launched with a specific version of the JVM.
ConditionalOnJndi The condition is satisfied only if a specific resource is available through JNDI.
ConditionalOnMissingBean The condition is met if the desired bean is missing in BeanFactory.
ConditionalOnMissingClass The condition is true if the required class is not in the classpath.
ConditionalOnNotWebApplication The condition is true if the application context is not a web context.
ConditionalOnProperty The condition is satisfied if the necessary parameters are specified in the settings file.
ConditionalOnResource The condition is satisfied if the desired resource is present in the classpath.
ConditionalOnSingleCandidate The condition is met if the bean of the specified class is already contained in the BeanFactory and it is the only one.
ConditionalOnWebApplication The condition is true if the application context is a web context.


All of them can be applied together over one definition of a bin.







Thus, @Conditional is a fairly powerful tool for configuring the context, allowing you to make applications even more flexible. But it is worth considering the fact that you need to use this annotation carefully, since the behavior of the context becomes not as obvious as when using profiles - with a large number of configured bins, you can quickly get confused. It is advisable to carefully document and log its use in your project, otherwise the code support will cause difficulties.








All Articles