1. Introduction

In this tutorial, we'll look at how we can create beans conditionally using Spring. For this purpose, we'll use the @Conditional annotation.

2. Sample Application

Let's first look at our sample application.

We'll use AuditService and its only implementation DefaultAuditService:

public interface AuditService {

    void audit();
}

@Component
public class DefaultAuditService implements AuditService {

    @Override
    public void audit() {
        // Do audit...
    }
}

3. Use Condition with @Conditional

To conditionally create a bean, we must first create an implementation of Condition. The Condition interface contains the matches method which returns a boolean value.

We'll now create one to conditionally create the AuditService bean:

public class AuditEnabledCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        final Environment environment = context.getEnvironment();
        return environment.getProperty("audit.enabled", Boolean.class, false);
    }
}

Here, the AuditEnabledCondition class is checking whether audit.enabled is true using the Environment properties.

Now that we have a Condition, we'll next apply it using the @Conditional annotation. @Conditional accepts an array of Condition-implementing classes in its value attribute. Then Spring creates an instance of each class, invokes their matches method and registers the bean if all conditions return true:

@Component
@Conditional(AuditEnabledCondition.class)
public class DefaultAuditService implements AuditService {    
    
    // Other code.
}

In this example, we're annotating the DefaultAuditService component with @Conditional(AuditEnabledCondition.class). As a result, Spring will only register this bean when AuditEnabledCondition returns true.

4. Use ConfigurationCondition with @Conditional

Most of the time, the Condition interface is what we need. In its implementations, we can check the application properties or talk to other systems. However, when we need to interact with bean instances, Condition isn't preferable. Instead, we must use the ConfigurationCondition interface which extends Condition. 

ConfigurationCondition adds another method, getConfigurationPhase. And the returned ConfigurationPhase determines the phase where Spring will evaluate the condition:

public interface ConfigurationCondition extends Condition {

    ConfigurationPhase getConfigurationPhase();
   
    // Other code...
}

ConfigurationPhase contains two values: PARSE_CONFIGURATION and REGISTER_BEAN. So if we register our Condition for the PARSE_CONFIGURATION phase, Spring will evaluate it when parsing the configurations and bean definitions. On the other hand, if we use the REGISTER_BEAN phase, Spring will parse the configurations as usual but will evaluate the condition when registering the beans.

Now, let's see an example where we register a bean according to some missing bean.

We'll first create another AuditService implementation:

@Component
public class DummyAuditService implements AuditService {
    
    // Other code.
}

We want to register DummyAuditService only when there is no other AuditService bean in the Spring container:

public class MissingServiceCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        final ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        final Map<String, AuditService> auditServiceBeans = beanFactory.getBeansOfType(AuditService.class);
        return auditServiceBeans.isEmpty();
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
}

Here, we have the MissingServiceCondition class implementing ConfigurationCondition. In the matches method, we're querying the container for AuditService beans using the BeanFactory instance. If there is no AuditService bean, it returns true. Note that we're also specifying our phase as ConfigurationPhase.REGISTER_BEAN.

Then, we'll update the DummyAuditService class:

@Component
@Conditional(MissingServiceCondition.class)
public class DummyAuditService implements AuditService {
    
    // Other code.
}

When we run the application, Spring doesn't register DummyAuditService as a bean. This is because there are two AuditService implementations - DefaultAuditService and DummyAuditService - and the DefaultAuditService bean is registered in the container.

If we use ConfigurationPhase.PARSE_CONFIGURATION instead, Spring evaluates our condition when parsing the bean definitions. Since the AuditService beans are parsed but not registered yet, the condition returns true. So we end up with two AuditService beans in the container.

5. Create Meta-Annotation with @Conditional

Lastly, we'll look at how we can create a meta-annotation with @Conditional. In our case, we'll create a meta-annotation to conditionally create beans according to some missing service:

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

    Class<?> value();
}

Here, we have the ConditionalOnMissingService annotation that is meta-annotated with @Conditional. It uses the MissingServiceCondition class. Also, notice that we're defining a value attribute. This attribute will hold the bean class that we'll query the Spring container for.

Now, let's examine MissingServiceCondition:

public class MissingServiceCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        final Map<String, Object> annotationAttributes = metadata
          .getAnnotationAttributes(ConditionalOnMissingService.class.getName());
        final Class<?> beanClass = (Class<?>) annotationAttributes.get("value");
        final Map<String, ?> auditServiceBeans = context.getBeanFactory().getBeansOfType(beanClass);
        return auditServiceBeans.isEmpty();
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
}

This implementation is very similar to the previous example except that we're getting the class value from the ConditionalOnMissingService annotation.

Then we'll place @ConditionalOnMissingService on a component:

@Component
@ConditionalOnMissingService(AuditService.class)
public class DummyAuditService implements AuditService {

    // Other code.
}

6. Summary

In this tutorial, we've investigated how we can create beans conditionally using the @Conditional annotation. We first examined the Condition interface that is suitable for most of our needs. Then we looked at using ConfigurationCondition to interact with bean instances. We also created a meta-annotation using @Conditional.

Lastly, check out the source code for all examples in this tutorial over on Github.