@Qualifier
Picks a specific bean at an injection point by name or by a custom qualifier annotation when several beans share a type.
@Qualifier("beanName")- Added in Spring Boot Spring Framework 2.5
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | String= "" | The qualifier to match: a bean name, or a value given to a bean with its own @Qualifier. |
Return value
n/aNarrows the candidates for an injection point to the bean whose qualifier value (or bean name) matches. Also usable as a meta-annotation for custom qualifier annotations.
Errors
No bean with a matching qualifier fails startup with NoSuchBeanDefinitionException.
@Qualifier chooses between beans of the same type at the point of injection. Put it on a constructor parameter, @Bean method parameter or field, and Spring injects only the bean whose qualifier matches. A bean's name always works as its qualifier, so @Qualifier("reportExecutor") finds the @Bean method reportExecutor() or the @Component("reportExecutor") class.
public ReportJob(@Qualifier("reportExecutor") ExecutorService executor) { ... }On both sides
You can also put @Qualifier("fast") on the bean itself, which gives it a qualifier value separate from its name. Then renaming the class or method does not break injection points that use "fast".
Custom qualifier annotations
String qualifiers are easy to mistype, and a typo only shows up at startup. A custom annotation meta-annotated with @Qualifier, such as @Replica, is checked by the compiler, shows up in "find usages" and documents meaning rather than a name. Put it on the bean and on each injection point.
@Qualifier and @Primary together
A qualifier at the injection point always wins over @Primary. A common, clear set-up is one @Primary bean for the usual case and @Qualifier wherever the special one is needed.
Mistakes to avoid
- Relying on the parameter name instead. Spring does fall back to matching the parameter name against bean names, but renaming a parameter then silently changes, or breaks, which bean is injected.
- Putting
@Qualifieron a constructor field in a Lombok-generated constructor, where it may not be copied to the parameter. Write the constructor yourself or configure Lombok to copy it. - Qualifying everything. If every injection point needs a qualifier, the beans probably deserve distinct types, such as
ReportingDataSourcewrappers, rather than a growing list of strings.
To get every bean of a type with its name, inject Map<String, PaymentGateway>. The keys are bean names, which is handy for picking an implementation from a request value without any if chain.
Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
(proxyBeanMethods = false)
class ExecutorConfig {
ExecutorService requestExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
ExecutorService reportExecutor() {
return Executors.newFixedThreadPool(2);
}
}
public class MonthlyReportJob {
private final ExecutorService executor;
// Bean name as qualifier: picks the bounded pool, not the request executor.
public MonthlyReportJob(("reportExecutor") ExecutorService executor) {
this.executor = executor;
}
public void run() {
executor.submit(() -> System.out.println("Building monthly report"));
}
}When to use it
- Sending report generation to a small fixed thread pool while requests use virtual threads
- Choosing express or standard shipping rates with a compiler-checked custom qualifier
- Selecting an export format from a request parameter through a Map of beans
Frequently asked questions
Does @Qualifier use the bean name?
Yes. If no bean declares a matching @Qualifier value, Spring matches the value against bean names, so @Qualifier("reportExecutor") finds the bean called reportExecutor.
Which wins, @Qualifier or @Primary?
@Qualifier. It narrows the candidates first; @Primary only breaks ties among what is left.
More examples
A custom qualifier annotation
The compiler checks @Express, and an IDE finds every use of it. There is no string to mistype.
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.stereotype.Component;
import org.springframework.stereotype.Service;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
@interface Express {
}
interface ShippingRate {
int pence(int grams);
}
@Component
class StandardRate implements ShippingRate {
public int pence(int grams) { return 300 + grams / 10; }
}
@Component
@Express
class ExpressRate implements ShippingRate {
public int pence(int grams) { return 900 + grams / 5; }
}
@Service
public class ShippingQuoteService {
private final ShippingRate express;
public ShippingQuoteService(@Express ShippingRate express) {
this.express = express;
}
public int expressQuote(int grams) {
return express.pence(grams);
}
}Picking an implementation by name at runtime
A GET /reports/42?format=pdf endpoint can pass the query value straight through; map the IllegalArgumentException for an unknown format to a 400 response rather than letting it become a 500. Supporting a new format is one new bean whose name is the format.
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface ExportFormat {
byte[] render(Object report);
}
@Component("csv")
class CsvExport implements ExportFormat {
public byte[] render(Object report) { return new byte[0]; }
}
@Component("pdf")
class PdfExport implements ExportFormat {
public byte[] render(Object report) { return new byte[0]; }
}
@Service
public class ExportService {
private final Map<String, ExportFormat> formats;
// Keys are bean names: "csv" and "pdf".
public ExportService(Map<String, ExportFormat> formats) {
this.formats = formats;
}
public byte[] export(String format, Object report) {
ExportFormat f = formats.get(format);
if (f == null) {
throw new IllegalArgumentException("Unsupported format: " + format);
}
return f.render(report);
}
}Qualifying a @Bean method parameter
@Qualifier works on @Bean method parameters exactly as on constructor parameters. Other beans asking for a plain HttpClient would still be ambiguous here, so in a real application one of the two would also be @Primary.
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
record PartnerApi(HttpClient client, String baseUrl) {
}
@Configuration(proxyBeanMethods = false)
public class PartnerConfig {
@Bean
HttpClient defaultHttpClient() {
return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
}
@Bean
HttpClient slowPartnerHttpClient() {
return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15)).build();
}
@Bean
PartnerApi partnerApi(@Qualifier("slowPartnerHttpClient") HttpClient client) {
return new PartnerApi(client, "https://partner.example/api");
}
}
Discussion