Spring @Qualifier, @Primary and Conditional Beans
Introduction
This tutorial explains how to tell Spring which bean to inject when several beans have the same type, and how to create a bean only under certain conditions. We will start with the problem: two implementations of one interface, and the startup failure that follows. We will then solve it in several ways -@Primary, @Qualifier, custom qualifier annotations, parameter names, and @Fallback - and see which wins when they are combined. Next we will turn to conditional beans: beans that exist only in certain profiles with @Profile, beans controlled by your own Condition class, and lastly Spring Boot's @ConditionalOnProperty and its relatives, which power Boot's auto-configuration.(This tutorial builds on Spring dependency injectionClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field and @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration.)
The problem: more than one candidate
Spring injects by type. As long as exactly one bean matches the type a constructor asks for, there is nothing to decide. The moment two beans implement the same interface, Spring cannot choose, and the context fails to start -Two PaymentGateway beans make injection ambiguous
//AmbiguousApp.java
package com.javabrahman.spring.choosing.problem;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface PaymentGateway {
String charge(double amount);
}
@Component
class StripeGateway implements PaymentGateway {
public String charge(double amount) {
return "Stripe charged " + amount;
}
}
@Component
class PayPalGateway implements PaymentGateway {
public String charge(double amount) {
return "PayPal charged " + amount;
}
}
@Service
class CheckoutService {
CheckoutService(PaymentGateway gateway) {
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class AmbiguousApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("Started with " + context.getBeanDefinitionCount() + " beans");
} catch (Exception e) {
Throwable cause = e;
while (!(cause instanceof NoUniqueBeanDefinitionException) && cause.getCause() != null) {
cause = cause.getCause();
}
System.out.println(cause.getMessage());
}
}
}
No qualifying bean of type 'com.javabrahman.spring.choosing.problem.PaymentGateway' available: expected single matching bean but found 2: payPalGateway,stripeGateway
@Primary: a default choice
@Primary marks one bean as the default for its type. Whenever an injection point is ambiguous, Spring picks the primary bean. It suits the common case of one main implementation and one or two special ones -
@Primary makes one PaymentGateway the default
//PrimaryApp.java
package com.javabrahman.spring.choosing.primary;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface PaymentGateway {
String charge(double amount);
}
@Component
@Primary
class StripeGateway implements PaymentGateway {
public String charge(double amount) {
return "Stripe charged " + amount;
}
}
@Component
class PayPalGateway implements PaymentGateway {
public String charge(double amount) {
return "PayPal charged " + amount;
}
}
@Service
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
String pay(double amount) {
return gateway.charge(amount);
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class PrimaryApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(CheckoutService.class).pay(25.0));
System.out.println("getBean(PaymentGateway.class) returns " + context.getBean(PaymentGateway.class).getClass().getSimpleName());
}
}
}
Stripe charged 25.0 getBean(PaymentGateway.class) returns StripeGateway
Explanation of the code
@PrimaryonStripeGatewayresolved the ambiguity in the constructor ofCheckoutService, and ingetBean()by type too.PayPalGatewayis still a bean. Code that wants it specifically can ask for it with@Qualifier, as shown next, and aList<PaymentGateway>still receives both.@Primaryworks on@Beanmethods as well. Only one bean per type should be primary; two primary beans of one type are ambiguous again.
@Qualifier: choosing at the injection point
@Primary decides on the bean side, once for everyone. @Qualifier decides on the injection side: each constructor parameter, setter or field can say which bean it wants. The simplest qualifier value is the bean name -
@Qualifier selects a bean by name for each injection point
//QualifierApp.java
package com.javabrahman.spring.choosing.qualifier;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface PaymentGateway {
String charge(double amount);
}
@Component
@Primary
class StripeGateway implements PaymentGateway {
public String charge(double amount) {
return "Stripe charged " + amount;
}
}
@Component
class PayPalGateway implements PaymentGateway {
public String charge(double amount) {
return "PayPal charged " + amount;
}
}
@Service
class RefundService {
private final PaymentGateway gateway;
// @Qualifier wins over @Primary: this service always uses PayPal
RefundService(@Qualifier("payPalGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
String refund(double amount) {
return "Refund via " + gateway.getClass().getSimpleName() + ": " + gateway.charge(-amount);
}
}
@Service
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) { // no qualifier: gets the @Primary bean
this.gateway = gateway;
}
String pay(double amount) {
return "Checkout via " + gateway.getClass().getSimpleName() + ": " + gateway.charge(amount);
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class QualifierApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(CheckoutService.class).pay(40.0));
System.out.println(context.getBean(RefundService.class).refund(15.0));
}
}
}
Checkout via StripeGateway: Stripe charged 40.0 Refund via PayPalGateway: PayPal charged -15.0
Explanation of the code
@Qualifier("payPalGateway")on the constructor parameter selected the bean namedpayPalGateway- the default name ofPayPalGateway.- An explicit qualifier wins over
@Primary:RefundServicegot PayPal although Stripe is primary, whileCheckoutService, which has no qualifier, got the primary Stripe bean. - A qualifier value does not have to be the bean name: putting
@Qualifier("refunds")on thePayPalGatewayclass as well gives it a qualifier label that injection points can use, independent of its name.
Custom qualifier annotations and parameter names
Qualifier strings are easy to mistype, and a typo is only found at startup. A custom qualifier annotation - your own annotation, itself annotated with@Qualifier - is checked by the compiler and documents the meaning. Spring also has a last-resort rule: if nothing else decides, it compares the parameter name with the bean names -
A custom @Instant qualifier annotation, and matching by parameter name
//CustomQualifierApp.java
package com.javabrahman.spring.choosing.custom;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@interface Instant {
}
interface Notifier {
String send(String message);
}
@Component
@Instant
class SmsNotifier implements Notifier {
public String send(String message) {
return "SMS: " + message;
}
}
@Component
class EmailNotifier implements Notifier {
public String send(String message) {
return "Email: " + message;
}
}
@Service
class FraudAlerts {
private final Notifier notifier;
FraudAlerts(@Instant Notifier notifier) { // type-safe: the compiler checks @Instant exists
this.notifier = notifier;
}
String alert() {
return notifier.send("Suspicious login");
}
}
@Service
class Newsletter {
private final Notifier notifier;
// No qualifier and no @Primary: the parameter name "emailNotifier" matches the bean name
Newsletter(Notifier emailNotifier) {
this.notifier = emailNotifier;
}
String send() {
return notifier.send("November issue");
}
}
@Configuration
@ComponentScan
class AppConfig {
}
public class CustomQualifierApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(FraudAlerts.class).alert());
System.out.println(context.getBean(Newsletter.class).send());
}
}
}
SMS: Suspicious login Email: November issue
Explanation of the code
@Instantis meta-annotated with@Qualifier, so Spring treats it as a qualifier. Putting it onSmsNotifierand on theFraudAlertsparameter connects the two. It needsRUNTIMEretention so that Spring can read it.Newsletterhas no qualifier, and neither notifier is primary, yet startup succeeded: Spring matched the parameter nameemailNotifierto the bean namedemailNotifier.- Matching by parameter name only works when the compiler keeps parameter names in the class files - the
-parametersoption ofjavac, which these examples use and which Spring Boot's Maven and Gradle plugins switch on. It is also easy to break by renaming a parameter, so treat it as a fallback and prefer an explicit qualifier.
@Fallback: the opposite of @Primary
@Fallback, added in Spring Framework 6.2, marks a bean that should be used only when no other candidate is available. It is ideal for a default implementation that an application - or a test - may replace simply by declaring another bean -
A @Fallback bean steps aside when another bean of the same type exists
//FallbackApp.java
package com.javabrahman.spring.choosing.fallback;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Fallback;
import org.springframework.context.annotation.Import;
interface CurrencyConverter {
String name();
}
class FixedRateConverter implements CurrencyConverter {
public String name() {
return "fixed rates (built-in default)";
}
}
class LiveRateConverter implements CurrencyConverter {
public String name() {
return "live rates";
}
}
@Configuration
class DefaultsConfig {
@Bean
@Fallback
CurrencyConverter fixedRateConverter() {
return new FixedRateConverter();
}
}
@Configuration
class LiveRatesConfig {
@Bean
CurrencyConverter liveRateConverter() {
return new LiveRateConverter();
}
}
@Configuration
@Import(DefaultsConfig.class)
class DefaultsOnly {
}
@Configuration
@Import({DefaultsConfig.class, LiveRatesConfig.class})
class DefaultsAndLiveRates {
}
public class FallbackApp {
public static void main(String[] args) {
for (Class<?> config : new Class<?>[] {DefaultsOnly.class, DefaultsAndLiveRates.class}) {
try (var context = new AnnotationConfigApplicationContext(config)) {
System.out.println(config.getSimpleName() + " -> " + context.getBean(CurrencyConverter.class).name());
}
}
}
}
DefaultsOnly -> fixed rates (built-in default) DefaultsAndLiveRates -> live rates
Explanation of the code
- With only the defaults, the fallback converter was the single candidate and was used.
- Once
LiveRatesConfigadded another converter, the@Fallbackbean stepped aside - with no@Primaryand no qualifier needed. - The order of precedence when Spring chooses among several candidates is: an explicit qualifier (which narrows the candidates first), then the
@Primarybean, then the only non-fallback bean, then the bean with the highest@Priority(fromjakarta.annotation), then a match on the parameter name. If none of these decides, startup fails withNoUniqueBeanDefinitionException.
Conditional beans with @Profile
Sometimes the question is not which bean to inject, but whether a bean should exist at all. The most common case is environments: a fake mail sender on a developer's machine, the real one in production.@Profile registers a bean only when the named profile is active -
@Profile registers different beans in different environments
//ProfileApp.java
package com.javabrahman.spring.choosing.profile;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
interface MailSender {
String send(String to);
}
@Component
@Profile("dev")
class ConsoleMailSender implements MailSender {
public String send(String to) {
return "[dev] mail to " + to + " printed to the console only";
}
}
@Component
@Profile("prod")
class SmtpMailSender implements MailSender {
public String send(String to) {
return "[prod] mail to " + to + " sent through SMTP";
}
}
@Component
@Profile("!prod") // every profile except prod
class SampleDataLoader {
}
@Configuration
@ComponentScan
class AppConfig {
}
public class ProfileApp {
public static void main(String[] args) {
for (String profile : new String[] {"dev", "prod"}) {
try (var context = new AnnotationConfigApplicationContext()) {
context.getEnvironment().setActiveProfiles(profile);
context.register(AppConfig.class);
context.refresh();
System.out.println(context.getBean(MailSender.class).send("Deborah Sprightly")
+ " | sample data loaded: " + context.containsBean("sampleDataLoader"));
}
}
}
}
[dev] mail to Deborah Sprightly printed to the console only | sample data loaded: true [prod] mail to Deborah Sprightly sent through SMTP | sample data loaded: false
Explanation of the code
- Each run activated one profile before registering the configuration and refreshing the context. Only the
MailSenderfor that profile became a bean, so there was never any ambiguity. @Profile("!prod")negates a profile:SampleDataLoaderexists in every environment except production. Profile expressions also support&and|, as in@Profile("dev & cloud").- Outside code, profiles are usually activated with the
spring.profiles.activeproperty - a JVM system property, an environment variable (SPRING_PROFILES_ACTIVE), or a line in Spring Boot'sapplication.properties.
Your own condition with @Conditional
@Profile is itself built on a more general mechanism: @Conditional, which takes a class implementing Condition. Spring calls the condition's matches() method while it reads the configuration, and registers the bean only if it returns true. The condition below enables a feature from a property -
A custom Condition that registers a bean based on a property
//ConditionalApp.java
package com.javabrahman.spring.choosing.conditional;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;
class RecommendationsEnabled implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "true".equals(context.getEnvironment().getProperty("features.recommendations"));
}
}
class RecommendationEngine {
}
@Configuration
class AppConfig {
@Bean
@Conditional(RecommendationsEnabled.class)
RecommendationEngine recommendationEngine() {
return new RecommendationEngine();
}
}
public class ConditionalApp {
public static void main(String[] args) {
for (String value : new String[] {"false", "true"}) {
System.setProperty("features.recommendations", value);
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println("features.recommendations=" + value
+ " -> RecommendationEngine bean exists: " + context.containsBean("recommendationEngine"));
}
}
}
}
features.recommendations=false -> RecommendationEngine bean exists: false features.recommendations=true -> RecommendationEngine bean exists: true
Explanation of the code
RecommendationsEnabled.matches()read the property through theConditionContext, which also gives access to the bean factory, the class loader and resources.- With the property
false, the@Beanmethod was skipped completely - the bean definition was never registered, so the method was never called. @Conditionalcan also go on a@Configurationor@Componentclass, to switch a whole group of beans on or off.
Spring Boot's ready-made conditions
WritingCondition classes for everyday checks would be tedious, so Spring Boot provides a set of conditional annotations. They are what auto-configuration is made of, and you can use them in your own configuration too -
@ConditionalOnProperty: a property has a given value, or is present.@ConditionalOnClass/@ConditionalOnMissingClass: a class is, or is not, on the classpath - this is how Boot knows which libraries you added.@ConditionalOnBean/@ConditionalOnMissingBean: another bean exists, or does not. Auto-configuration declares its beans with@ConditionalOnMissingBean, which is why declaring your own bean of the same type replaces Boot's.@ConditionalOnWebApplication,@ConditionalOnResource,@ConditionalOnExpressionand more.
@ConditionalOnProperty instead of a hand-written condition, run once without and once with the property on the command line -
@ConditionalOnProperty in a Spring Boot application
//BootConditionsApp.java
package com.javabrahman.spring.choosing.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
class RecommendationEngine {
}
@SpringBootApplication
public class BootConditionsApp {
@Bean
@ConditionalOnProperty(name = "features.recommendations", havingValue = "true")
RecommendationEngine recommendationEngine() {
return new RecommendationEngine();
}
public static void main(String[] args) {
try (var context = SpringApplication.run(BootConditionsApp.class, args)) {
System.out.println("Arguments " + String.join(" ", args)
+ " -> RecommendationEngine bean exists: " + context.containsBean("recommendationEngine"));
}
}
}
Arguments --features.recommendations=false -> RecommendationEngine bean exists: false
Arguments --features.recommendations=true -> RecommendationEngine bean exists: true
Explanation of the code
@ConditionalOnProperty(name = ..., havingValue = "true")does the same job as ourRecommendationsEnabledclass, in one line.- Spring Boot turns command-line arguments of the form
--name=valueinto properties, so the same jar can switch the feature on or off without a code change. The same property could come fromapplication.propertiesor an environment variable (FEATURES_RECOMMENDATIONS=true). - Boot's conditions are tuned for configuration classes and auto-configuration; for a plain
@Component,@Profileor@ConditionalOnPropertyon the class works the same way.
Summary
In this tutorial we first saw the startup failure that occurs when two beans match one injection point. We resolved it with@Primary, which sets a default bean, and with @Qualifier, which chooses at the injection point and wins over @Primary. We then wrote a type-safe custom qualifier annotation, saw Spring fall back to matching parameter names, and used @Fallback for a default bean that steps aside, which gave us the full order of precedence. Next we made beans conditional: on the active profile with @Profile, on a property with our own Condition, and lastly with Spring Boot's @ConditionalOnProperty and its related annotations. The last tutorial of this module covers expressions in configuration: SpEL and @ValueClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained.Spring Tutorials
Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session
Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More Spring Expression Language (SpEL) and @Value ExplainedClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session