Skip to main content

@Autowired Explained: By-Type Resolution, @Qualifier, @Primary, ObjectProvider and List Injection

How Spring chooses between several beans of the same type: the real NoUniqueBeanDefinitionException, @Qualifier, @Primary, @Priority, @Fallback and parameter-name matching in their measured order, then Optional, ObjectProvider, and List and Map injection. Tested on Spring Boot 4.1.1, Spring Framework 7.0.9 and Java 25.

Here is one class and two builds. The class asks for a PaymentGateway and calls its constructor parameter paypalGateway; the application has two gateway beans. Compile the class with one compiler flag and the application starts and injects the PayPal gateway. Compile the same source without the flag and it refuses to start with expected single matching bean but found 2. Nothing in the source changed, and there is no annotation that says which build you are in. That behaviour is the last rung of a ladder Spring climbs every time it has to choose between beans, and most explanations of @Autowired stop at the first rung. This article walks the whole ladder with a small payment-gateway example, prints the real exception for each way of getting it wrong, and then covers the injection points that take several beans at once: List, Map, Optional and ObjectProvider. Every code block links to a file in the core-di module of a companion repository that compiles and runs, and every console block is quoted from a transcript that test run wrote, not typed in by hand.
Versions. Spring Boot 4.1.1 and Spring Framework 7.0.9 (both poms were published to Maven Central on 20 August 2026), on Java 25 (LTS, Temurin 25.0.4.1). The scenarios run on a plain Spring ApplicationContext, so only the container’s own rules are in play. If you have not read it, the previous article on constructor, setter and field injection explains how the handed-over bean reaches your class; this one is about which bean gets handed over.

@Autowired asks one question first: which beans have this type?

Spring keeps a catalogue of every bean it has built or can build. When a class asks for a collaborator — with @Autowired, or simply as a constructor parameter, as covered in the previous article — the container does not go looking for a bean by a name you gave. It looks at the type of the parameter and collects every bean assignable to it. What happens next depends only on how many it found.
PaymentGateway the injection point 0 beans of that type 1 bean of that type 2 or more of that type NoSuchBeanDefinition start-up fails injected no rules needed a winner, or NoUnique depends on the hints The type is the first filter; every other hint only chooses between beans of the right type.
Zero candidates is an error, exactly one is the easy case, and two or more send Spring into its tie-break rules. That is why a project with one implementation per interface never has to think about any of this, and why the problem appears the day someone adds a second implementation. The rest of the article is the bottom branch. Take the simplest possible consumer, one that asks for a gateway and does nothing else (CheckoutPlain.java):
/** Asks for a PaymentGateway and says nothing more. */
public class CheckoutPlain {

    private final PaymentGateway gateway;

    public CheckoutPlain(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public String gatewayClass() {
        return gateway.getClass().getSimpleName();
    }
}
Register only that class, with no gateway bean at all, and the top branch of the diagram plays out. 12-missing-bean.txt is the transcript:
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
top message   : Error creating bean with name 'checkoutPlain': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

root cause    : org.springframework.beans.factory.NoSuchBeanDefinitionException
root message  : No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
The fingerprint of the zero-candidate case. expected at least 1 bean which qualifies as autowire candidate at the bottom, wrapped in an UnsatisfiedDependencyException whose message names the consumer (checkoutPlain) and the exact parameter (constructor parameter 0). The last part, Dependency annotations: {}, lists the annotations Spring saw on the injection point; empty means there was no @Qualifier or anything else narrowing the request. Read that line first when you are unsure whether a qualifier is being applied.
Going deeper: reading the exception chain

Both transcripts in this section print two exceptions: the top one, which is what Spring throws out of refresh(), and the root cause underneath. The top one, UnsatisfiedDependencyException, tells you where the problem is — which bean, which constructor parameter — and the root cause tells you what is wrong. In a Spring Boot application the same chain is turned into the formatted “APPLICATION FAILED TO START” report, so the two pieces of information are still there, only laid out differently.

The two root exceptions are related: NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionException (checked with javap against spring-beans-7.0.9.jar). A catch (NoSuchBeanDefinitionException e) therefore catches both “none” and “too many”, which is worth remembering the first time a fallback path swallows the wrong one. The helper that turns a failed start into a transcript is Ctx.java.

Going deeper

Two candidates and no hint: the exact error, and what it is telling you

Now give the container two PaymentGateway beans, StripeGateway and PaypalGateway (both are one-line subclasses of a logging gateway, for example StripeGateway.java), and the same unchanged CheckoutPlain. The scenario is a single line of test code:
            var outcome = Ctx.tryStart(StripeGateway.class, PaypalGateway.class, CheckoutPlain.class);
The context does not start. 10-no-unique-bean.txt has the messages:
started: false
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
top message   : Error creating bean with name 'checkoutPlain': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway

root cause    : org.springframework.beans.factory.NoUniqueBeanDefinitionException
root message  : No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway
The fingerprint of the ambiguous case. expected single matching bean but found 2: stripeGateway,paypalGateway. The two names at the end are the bean names Spring will accept in a @Qualifier, so the message is also the list of valid answers. Stripe was registered first and it did not win: registration order is not a tie-breaker, so “the first one” is never what you get.
The container has refused to guess, which is the right behaviour: choosing a payment provider by accident is the kind of mistake that reaches production. Your job is to give it a rule, and there are five rules to choose from. The next section lays them out and shows which wins when several apply at once.
Going deeper: why so many start-up failures are this one exception

Interface-with-several-implementations is the normal shape of a codebase that has grown: two payment providers, two notification channels, a real client and a stub. The failure is deliberately loud and deliberately at start-up, because a bean that was silently chosen would only reveal itself as wrong behaviour at run time. The error message includes the candidate names for that reason.

The same message appears when you did not add a second implementation yourself: a library or a Spring Boot auto-configuration may register one. If the count in the message is higher than you expect, list the beans (the dependency graph printed by getDependenciesForBean in 17-dependency-graph.txt is one way) before reaching for an annotation.

Going deeper

Five hints, and the order Spring tries them

There are five ways to tell Spring which bean you mean. Three go on the beans (@Primary, @Priority, @Fallback), one goes on the injection point (@Qualifier), and one is implicit (the name). @Qualifier goes on the injection point and names the bean you want. It is the most explicit, and as row 2 of the table below shows, it overrides a @Primary bean (CheckoutQualified.java):
public class CheckoutQualified {

    private final PaymentGateway gateway;

    public CheckoutQualified(@Qualifier("paypalGateway") PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public String gatewayClass() {
        return gateway.getClass().getSimpleName();
    }
}
@Primary goes on one bean and says “when there is a tie, prefer me” (PrimaryStripeGateway.java):
@Primary
public class PrimaryStripeGateway extends StripeGateway {
}
@Priority (from jakarta.annotation) puts a number on a bean; the lowest number wins (PriorityTwoGateway.java):
@Priority(2)
public class PriorityTwoGateway extends PaypalGateway {
}
@Fallback marks a bean that should be used only when nothing else qualifies (FallbackGateway.java). The fifth, implicit hint is the name of the parameter or field, covered in the next section because it has a catch of its own.
@Qualifier filters the list @Primary one bean wins name match param or field name @Priority lowest number wins default bean @Fallback loses The first step that finds a winner ends the search. @Qualifier removes candidates before the others run. Steps 2 to 5 follow the order of the calls in the bytecode below; the table rows are measured.
Read the boxes left to right as the order of the search. @Qualifier narrows the candidate list first; if that leaves one bean, the search is over. Otherwise Spring looks for a @Primary bean, then for a bean whose name equals the parameter or field name, then for the highest @Priority, and finally it prefers a bean that is not a @Fallback. The next table shows what actually happened when each combination was registered. 11-resolution-ladder.txt starts a fresh mini-context per row and prints which class the consumer received:
beans registered / injection point                         winner
---------------------------------------------------------- --------------------
@Primary Stripe + Paypal; CheckoutPlain                    PrimaryStripeGateway
@Primary Stripe + Paypal; @Qualifier("paypalGateway")      PaypalGateway
@Priority(1) Stripe + @Priority(2) Paypal; CheckoutPlain   PriorityOneGateway
@Priority(1) + Paypal(no priority); param named paypalGateway PaypalGateway
Stripe + Paypal (plain); param named paypalGateway         PaypalGateway
Stripe (plain) + @Fallback Paypal; CheckoutPlain           StripeGateway
@Fast custom qualifier, FastGateway + Paypal; @Fast param  FastGateway
Rows 1 and 2 show @Primary deciding a tie and a @Qualifier overriding it. Row 3 shows @Priority deciding a tie between two annotated beans. Row 6 shows @Fallback losing to an ordinary bean even though it was declared alongside it.
The surprise is row 4. One bean carries @Priority(1), the other has no annotation but its name matches the parameter name, and the unannotated bean wins. If you assume “annotations beat names”, this is the row that breaks the assumption. It is measured on Spring Framework 7.0.9 and it agrees with the order of the calls in the bytecode below; do not build a design around a subtle precedence between two hints. If you need a specific bean, say so with @Qualifier.
Going deeper: reading the choice straight out of the framework

The method that makes this decision is DefaultListableBeanFactory.determineAutowireCandidate. Its instruction listing from spring-beans-7.0.9.jar is 23-determine-autowire-candidate-bytecode.txt; the method calls, in order of appearance, are the ladder in the diagram:

# jar: spring-beans-7.0.9.jar

  protected java.lang.String determineAutowireCandidate(java.util.Map<java.lang.String, java.lang.Object>, org.springframework.beans.factory.config.DependencyDescriptor);
invokevirtual #1076               // Method org/springframework/beans/factory/config/DependencyDescriptor.getDependencyType:()Ljava/lang/Class;
invokevirtual #1039               // Method determinePrimaryCandidate:(Ljava/util/Map;Ljava/lang/Class;)Ljava/lang/String;
invokevirtual #1169               // Method org/springframework/beans/factory/config/DependencyDescriptor.getDependencyName:()Ljava/lang/String;
invokeinterface #970,  1          // InterfaceMethod java/util/Map.keySet:()Ljava/util/Set;
invokeinterface #397,  1          // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304,  1          // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309,  1          // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokevirtual #1393               // Method matchesBeanName:(Ljava/lang/String;Ljava/lang/String;)Z
invokevirtual #150                // Method getAutowireCandidateResolver:()Lorg/springframework/beans/factory/support/AutowireCandidateResolver;
invokeinterface #1172,  2         // InterfaceMethod org/springframework/beans/factory/support/AutowireCandidateResolver.getSuggestedName:(Lorg/springframework/beans/factory/config/DependencyDescriptor;)Ljava/lang/String;
invokeinterface #970,  1          // InterfaceMethod java/util/Map.keySet:()Ljava/util/Set;
invokeinterface #397,  1          // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304,  1          // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309,  1          // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokevirtual #1393               // Method matchesBeanName:(Ljava/lang/String;Ljava/lang/String;)Z
invokevirtual #1043               // Method determineHighestPriorityCandidate:(Ljava/util/Map;Ljava/lang/Class;)Ljava/lang/String;
invokevirtual #1046               // Method determineDefaultCandidate:(Ljava/util/Map;)Ljava/lang/String;
invokeinterface #1230,  1         // InterfaceMethod java/util/Map.entrySet:()Ljava/util/Set;
invokeinterface #397,  1          // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304,  1          // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309,  1          // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokeinterface #1235,  1         // InterfaceMethod java/util/Map$Entry.getKey:()Ljava/lang/Object;
invokeinterface #1238,  1         // InterfaceMethod java/util/Map$Entry.getValue:()Ljava/lang/Object;
invokeinterface #1399,  2         // InterfaceMethod java/util/Map.containsValue:(Ljava/lang/Object;)Z

Reading it top to bottom: determinePrimaryCandidate first; then a loop that compares each candidate name with getDependencyName via matchesBeanName, and again with the name suggested by the candidate resolver; then determineHighestPriorityCandidate; then determineDefaultCandidate. The loop at the end checks whether a candidate is one of the container’s own resolvable dependencies. The script that produces the listing is capture-bytecode.sh. Bytecode tells you the order of the calls, not the reason for each; the reasons come from the reference documentation linked below.

Going deeper: a custom qualifier instead of a string

A @Qualifier("paypalGateway") repeats a bean name in every place it is used, and a typo is a runtime failure. A custom qualifier turns the string into a type the compiler checks. It is an annotation that is itself annotated @Qualifier (Fast.java):

/** A custom qualifier: a meta-annotation over {@code @Qualifier}, so call sites do not repeat a string. */
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fast {
}

Put it on a bean and on the injection point (CheckoutFast.java):

public class CheckoutFast {

    private final PaymentGateway gateway;

    public CheckoutFast(@Fast PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public String gatewayClass() {
        return gateway.getClass().getSimpleName();
    }
}

The last row of the table above is that scenario: FastGateway, marked @Fast, is chosen although a second gateway is registered. The reference chapter on qualifiers also covers qualifier attributes and using several qualifiers together.

Going deeper: @Resource chooses by name first

@Resource comes from jakarta.annotation, not from Spring, and it reverses the order of questions: it looks for a bean named after the field first, and falls back to the type. Both forms are in ResourceConsumer.java and the result is in 16-generics-and-resource.txt:

@Resource             : paypalGateway field -> PaypalGateway, whateverIWantToCallIt field -> StripeGateway

The plain @Resource on a field called paypalGateway got the PayPal bean; the explicit name = "stripeGateway" on a field with an unrelated name got the Stripe bean. Note the caveat that goes with any name-driven lookup: the field name is a hint that a rename can silently change.

Going deeper

Matching by parameter name only works if the compiler kept the names

The implicit hint deserves its own section, because it is the one that fails without any annotation to look at. When several beans tie, one of the things Spring compares is the name of the parameter or field with the bean names. A constructor parameter called paypalGateway selects the bean called paypalGateway (CheckoutNamed.java):
/** The parameter name is the only hint: it matches the bean name {@code paypalGateway}. */
public class CheckoutNamed {

    private final PaymentGateway paypalGateway;

    public CheckoutNamed(PaymentGateway paypalGateway) {
        this.paypalGateway = paypalGateway;
    }

    public String gatewayClass() {
        return paypalGateway.getClass().getSimpleName();
    }
}
There is nothing here that says “use PayPal” except an identifier. That works only if the compiled class file still contains the identifier, and by default javac discards parameter names. To prove it, the test in CandidateResolutionTest.java compiles a class of this shape twice from the same source text, once with -parameters and once without, and loads each into a context that has both gateways:
NamedConsumer.java param named paypalGateway javac -parameters names kept in the class javac (no flag) names dropped started picked PaypalGateway NoUniqueBeanDefinition found 2: stripe, paypal One source file, two builds, two outcomes: the hint is in the compiler flag, not in the code.
The left box is a single source file; the two middle boxes are the two builds; the right boxes are what Spring did. The only difference between the outcomes is a flag handed to the compiler, which is why this failure looks inexplicable to anyone reading only the source. It also explains a familiar situation: the code works from the IDE and fails in a different build, or the other way round. The transcript is 18-parameters-flag.txt:
javac -parameters  : started, picked PaypalGateway
javac (no flag)    : FAILED NoUniqueBeanDefinitionException
    No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway
The fingerprint. expected single matching bean but found 2 for a class that looks as if it names its bean. If a parameter or field name is the only thing selecting a bean, and a build that used to work starts failing after a build-tool or compiler-option change, check the -parameters flag first.
Spring Boot projects rarely meet this because the Boot parent pom turns the flag on. 26-boot-parent-parameters-flag.txt is the relevant lines from spring-boot-starter-parent-4.1.1.pom:
111-          <artifactId>maven-compiler-plugin</artifactId>
112-          <configuration>
113:            <parameters>true</parameters>
114-          </configuration>
115-        </plugin>
Going deeper: whether to rely on it at all

The name fallback is convenient and fragile in equal measure. It disappears when the flag does, it changes when someone renames a field for unrelated reasons, and it is invisible to readers. It is reasonable for a throwaway class in a small application where the two beans are obviously distinguished by name; for anything a colleague will maintain, a @Qualifier or a custom qualifier costs one line and survives a rename. The measurement above is the whole of what was verified here: on 7.0.9, without the flag, the fallback did not happen.

The flag matters beyond this one feature — anything in Spring that reads parameter names from the class file depends on it — and the Boot parent pom’s setting is why most Boot applications never think about it. If your build does not inherit from spring-boot-starter-parent, you own this setting.

Going deeper

When the bean may not exist: Optional, @Nullable and ObjectProvider

Sometimes a dependency is genuinely optional: an audit sink that exists only in some environments, a metrics client that a test profile leaves out. Spring gives you four ways to say “this may be absent” (the plain parameter is in the transcript for comparison), and they agree while the bean is missing and disagree as soon as there are two of them. The class that holds them all is OptionalConsumers.java; the provider variant is representative:
    public static class WithProvider {
        public final ObjectProvider<Notifier> provider;

        public WithProvider(ObjectProvider<Notifier> provider) {
            this.provider = provider;
        }
    }
With zero Notifier beans, 13-optional-and-objectprovider.txt shows:
--- zero Notifier beans ---
plain constructor param          : FAILED NoSuchBeanDefinitionException
Optional<Notifier>               : isPresent=false
@Nullable Notifier               : value=null
ObjectProvider.getIfAvailable()  : null
ObjectProvider.getIfUnique()     : null
ObjectProvider.getObject()       : NoSuchBeanDefinitionException: No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
@Autowired(required=false) setter: value=null
A plain parameter fails, as it should. Optional is empty, @Nullable receives null, an ObjectProvider answers null from getIfAvailable() and getIfUnique() but throws from getObject(), and the setter marked required = false is simply not called. Now register two notifiers, SmsNotifier (@Order(1)) and EmailNotifier (@Order(2)), with Email registered first:
--- two Notifier beans (Sms, Email), no @Primary ---
ObjectProvider.getIfAvailable()  : NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected single matching bean but found 2: emailNotifier,smsNotifier
ObjectProvider.getIfUnique()     : null
ObjectProvider.stream() classes  : [EmailNotifier, SmsNotifier]
ObjectProvider.orderedStream()   : [SmsNotifier, EmailNotifier]
Optional<Notifier> with two beans: FAILED NoUniqueBeanDefinitionException
  No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected single matching bean but found 2: emailNotifier,smsNotifier
Optional is not a tie-breaker. Optional<Notifier> reads as “one or nothing”, but two beans is neither, and the injection fails with the same NoUniqueBeanDefinitionException as a plain parameter. ObjectProvider.getIfAvailable() throws the same exception; getIfUnique() is the method that returns null for “not exactly one”. Choose the method by what you want when the count is not one.
stream() and orderedStream() are how you take all of them: the first followed registration order (Email, Sms) and the second followed @Order (Sms, Email).
Going deeper: what an ObjectProvider actually is

An ObjectProvider<T> is a small handle to the container’s lookup for T. The dependency is not resolved when your class is built; it is resolved each time you call a method on the handle. That is why it can express “maybe none”, “maybe several” and “a fresh one each time” — the last of which is how it repairs a prototype bean injected into a singleton, demonstrated in the article on bean scopes.

The nullability annotation used in the repository’s @Nullable variant is org.jspecify.annotations.Nullable; Spring’s own org.springframework.lang.Nullable is deprecated since 7.0, as the previous article verified from the class file.

Going deeper

Injecting every bean of a type: List, Map and Set

The cleanest answer to “which of my three implementations?” is often “all of them”. Ask for a collection and Spring injects every bean of the element type, which is how a set of notification channels, validators or plug-ins gets wired without an if chain (Broadcaster.java):
/** Receives every Notifier three ways: as a List, a Map keyed by bean name, and a Set. */
public class Broadcaster {

    public final List<Notifier> asList;
    public final Map<String, Notifier> asMap;
    public final Set<Notifier> asSet;

    public Broadcaster(List<Notifier> asList, Map<String, Notifier> asMap, Set<Notifier> asSet) {
        this.asList = asList;
        this.asMap = asMap;
        this.asSet = asSet;
    }
}
Three notifiers were registered in the order Push, Email, Sms, and 14-list-map-set-injection.txt shows what each shape received:
List<Notifier> order   : [SmsNotifier, EmailNotifier, PushNotifier]
Map<String,Notifier>   : [pushNotifier, emailNotifier, smsNotifier]
Set<Notifier> size     : 3
PushNotifier registered 1st, no @Order EmailNotifier registered 2nd, @Order(2) SmsNotifier registered 3rd, @Order(1) container every Notifier bean List: [Sms, Email, Push] sorted by @Order, unordered last Map: [push, email, sms] key = bean name; registration order here Set: 3 elements no order to rely on
The List is sorted: @Order(1) on SmsNotifier puts it first, @Order(2) puts Email second, and PushNotifier, which has no annotation, comes last even though it was registered first. The Map is keyed by bean name, which makes it the natural shape for “pick the strategy by a string from a request”. In this run its keys came out in registration order; only that one registration order was measured, so do not read more into it, and put the order you need in @Order on beans you take as a List. The order is worth caring about when the collection is a chain: authorisation checks that must run before logging, or a list of fallbacks tried in turn. Without @Order, the List order is whatever the container happens to produce, which is not something to build a chain on.
Going deeper: the generic type is part of the request

An injection point List<Handler<String>> collects only the beans that are Handler<String>, and a parameter Handler<Integer> picks the Handler<Integer> bean without any qualifier, although both implement the same interface (GenericConsumer.java):

/** The generic type argument is part of the injection point: only Handler&lt;String&gt; beans arrive. */
public class GenericConsumer {

    public final List<Handler<String>> stringHandlers;
    public final Handler<Integer> integerHandler;

    public GenericConsumer(List<Handler<String>> stringHandlers, Handler<Integer> integerHandler) {
        this.stringHandlers = stringHandlers;
        this.integerHandler = integerHandler;
    }
}

The result, from 16-generics-and-resource.txt:

List<Handler<String>>  : [StringHandler]
Handler<Integer>       : IntegerHandler

So generics are a fourth kind of narrowing that costs nothing to write: if your two implementations differ in a type argument, the tie never happens.

Going deeper

The empty-collection trap: constructors say yes, fields and setters say no

One more case sits between “required” and “optional”: a collection of beans when zero beans of that type exist. A plug-in host with no plug-ins installed is a perfectly reasonable state. Whether it starts depends on which injection style you chose, and the four hosts in PluginHosts.java were written to show it. The constructor form and the field form differ by a few lines:
    public static class Plain {
        public Plain(List<Plugin> plugins) {
        }
    }
    public static class FieldInjected {
        @Autowired
        public List<Plugin> plugins;
    }
There are no Plugin beans in any of these runs. 15-empty-collection.txt shows the outcome:
--- required List<Plugin>, zero Plugin beans ---
single constructor param      : started (empty list injected)
@Autowired ctor + no-arg ctor  : started (empty list injected)
FieldInjected              : FAILED NoSuchBeanDefinitionException
    No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
SetterInjected             : FAILED NoSuchBeanDefinitionException
    No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
The constructor versions start — Spring injects an empty list — including a class that has two constructors with the one taking the list marked @Autowired. The field and the setter versions fail with a NoSuchBeanDefinitionException, although the injected type is the same. If you want an absent collection to be legal at every injection point, the second half of the transcript shows four ways:
--- ways to make an absent collection legal ---
Optional<List<Plugin>>          : isPresent=false
@Nullable List<Plugin>          : value=null
ObjectProvider<Plugin> -> list  : []
@Autowired(required=false) list: []
The fingerprint. No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>' — a collection type inside the quotes — and, on the field variant, Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} showing that required=true is the default. A refactor from a constructor to a field can turn “starts with no plug-ins” into “will not start”, and nothing in the diff looks like a behaviour change.
Going deeper: what to do about it

The practical rule is to say what you mean. If “no plug-ins” is legal, spell it: @Autowired(required = false), an Optional<List<Plugin>>, a @Nullable list, or an ObjectProvider and orderedStream(). The last is the version that also tolerates two or more beans and keeps them in @Order, which is why it is the one used in WithProvider. Do not depend on the constructor leniency: it is a fallback for one style, not a contract for the design, and the previous article shows how easily the rules for choosing a constructor change when a second one is added.

Going deeper

So which hint, and when

The table collects the whole article as a set of situations. Read it as “what do I want the code to say”.
You wantWriteWhy
exactly this bean, always@Qualifier or a custom qualifier at the injection pointexplicit, survives renames when the qualifier is a type
a sensible default with exceptions@Primary on the default beanone annotation, and a @Qualifier still overrides it
a bean only when nothing else exists@Fallbackthe bean steps aside for any ordinary candidate
all of themList<T> or Map<String, T>no tie to break; order with @Order
it may not existObjectProvider<T> or an optional setterabsence is written in the type; pick the method for the count you want
a collection that may be emptysay so explicitlyfield and setter injection do not tolerate an empty collection
to rely on a parameter nameavoid itit needs -parameters and is invisible to readers
Should you even care, on a small project? If every interface in your application has one implementation, none of this will ever fire, and you can leave it unread. It starts to matter the day a second implementation appears — often by adding a stub, a second provider or a library that registers its own bean — and the message at that point is a start-up failure, which is the cheapest place to learn it. Two points are opinion rather than measurement: prefer an explicit @Qualifier at the injection point over @Primary when a wrong choice would be costly, because it reads as a decision at the call site; and treat a growing pile of hints as a sign that the abstraction is doing two jobs.
Going deeper: hints in the reference documentation

The reference chapter on @Autowired (Using @Autowired) covers the annotation itself, its use on constructors, setters and fields, and collections; the pages on @Primary and @Fallback and qualifiers cover the two hints in detail. Everything they document in prose has a runnable counterpart in this article’s transcripts, which is the reason to trust the parts where this article says “measured” over anything remembered from an older Spring version.

Going deeper

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.