DEV Community

Cover image for Conditional Dependency Injection In Spring Framework And Spring Boot
İbrahim Gündüz
İbrahim Gündüz

Posted on • Originally published at Medium

Conditional Dependency Injection In Spring Framework And Spring Boot

Problem:

To test the application code with minimal mocking effort, we want to make the payment processor implementation configurable so that the application can use the dummy processor if none is configured.

Solutions:

1- Factory Method In The Configuration Class

With this solution, you can create beans conditionally by a factory method created in the configuration class. You can create an instance of the desired implementation based on a system property and an instance of fallback implementation otherwise. It doesn’t create extra beans.

@Configuration
public class AppConfig {
    private final Environment environment;

    public AppConfig(Environment environment) {this.environment = environment;}

    @Bean
    public PaymentProcessor paymentProcessor() {
        if (environment.getProperty("payment.processor.enabled", Boolean.class, false)) {
            return new PayuPaymentProcessor();
        } else {
            return new NoopPaymentProcessor();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s test it and see how it works.

import org.example.AppConfig;
import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class ConditionalDiTest {
    @Test
    public void testWithNoopPaymentProcessorImp() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);

        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
    }

    @Test
    public void testWithCurrentPaymentProcessorImpl() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        ConfigurableEnvironment environment = context.getEnvironment();
        environment.getPropertySources()
                .addFirst(new MapPropertySource("test", Map.of("payment.processor.enabled", "true")));

        context.register(AppConfig.class);

        context.refresh();

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Conditional Annotation

The @Conditional annotation allows the registration of beans based on custom conditions. It can be defined at the class or method level. You can define the logic that determines whether the bean should be created by creating a class that implements org.springframework.context.annotation.Condition interface.

To implement this solution, let’s create our condition classes like the ones below:

public class PaymentProcessorEnabledCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("payment.processor.enabled", Boolean.class, false);
    }
}


public class PaymentProcessorFallbackCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return !context.getEnvironment().getProperty("payment.processor.enabled", Boolean.class, false);
    }
}
Enter fullscreen mode Exit fullscreen mode

Add @Conditional annotation to the implementation classes and pass the appropriate condition classes as arguments.

@Component
@Conditional(PaymentProcessorEnabledCondition.class)
public class PayuPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return null;
    }
}


@Component
@Conditional(PaymentProcessorFallbackCondition.class)
public class NoopPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, run the following test to see how it works.

import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class ConditionalInjectionTest {
    @Test
    public void testWithNoopPaymentProcessorImp() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        context.scan("org.example");
        context.refresh();

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);

        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
    }

    @Test
    public void testWithCurrentPaymentProcessorImpl() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        ConfigurableEnvironment environment = context.getEnvironment();
        environment.getPropertySources()
                .addFirst(new MapPropertySource("test", Map.of("payment.processor.enabled", "true")));

        context.scan("org.example");
        context.refresh();

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
    }
}
Enter fullscreen mode Exit fullscreen mode

3. ConditionalOnProperty Annotation

This annotation registers the bean only if the specified property is present in the application config and, optionally has a specified value. This annotation is available only in Spring Boot.

@Component
@ConditionalOnProperty(name = "payment.processor.enabled", havingValue = "true")
public class PayuPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return null;
    }
}


@Component
@ConditionalOnProperty(name = "payment.processor.enabled", havingValue = "false", matchIfMissing = true)
public class NoopPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
    }
}
Enter fullscreen mode Exit fullscreen mode

You can run the same test code from the previous example.

3. @profile Annotation

Profile annotation allows the registration of beans based on the active application profile.

package org.example;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("production")
public class PayuPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return null;
    }
}

@Component
@Profile("default")
public class NoopPaymentProcessor implements PaymentProcessor {
    @Override
    public AuthorizationResource authorize(AuthorizationCreate command) {
        return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
    }
}
Enter fullscreen mode Exit fullscreen mode

You can test the code above, just by changing the active profile in the test code as below

import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class ConditionalInjectionTest {
    @Test
    public void testWithNoopPaymentProcessorImp() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        context.scan("org.example");
        context.refresh();

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);

        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
    }

    @Test
    public void testWithCurrentPaymentProcessorImpl() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        ConfigurableEnvironment environment = context.getEnvironment();
        environment.setActiveProfiles("production");

        context.scan("org.example");
        context.refresh();

        PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
        assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading! You can find all the examples in the following repository.

Code Example

Credits

Top comments (0)