Add core-di: constructor vs setter vs field injection and @Autowired candidate resolution on Boot 4.1

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JoVmf2fWvcpoXndcDSwRf7
This commit is contained in:
Claude
2026-09-24 04:59:40 +00:00
parent b0c39a8d4c
commit 9825efa081
87 changed files with 1945 additions and 0 deletions
@@ -0,0 +1,18 @@
# The same OrderService built with plain 'new', no Spring anywhere
--- constructor injection ---
new ConstructorOrderService(gateway, notifier).place("A-1", 500)
OK -> charged 500 cents for A-1
charges=1, notifications=1
--- setter injection, one setter forgotten ---
new SetterOrderService(); setter.setGateway(gateway); // setNotifier never called
NullPointerException: Cannot invoke "com.ankurm.coredi.injection.Notifier.send(String)" because "this.notifier" is null
charges=1 <-- the customer was charged before the failure
--- field injection ---
new FieldOrderService().place("A-3", 500)
NullPointerException: Cannot invoke "com.ankurm.coredi.injection.PaymentGateway.charge(String, int)" because "this.gateway" is null
after ReflectionTestUtils.setField(...) twice:
OK -> charged 500 cents for A-3
@@ -0,0 +1,8 @@
# Which injected fields can be final? (reflection over the three variants)
ConstructorOrderService gateway final=true
ConstructorOrderService notifier final=true
SetterOrderService gateway final=false
SetterOrderService notifier final=false
FieldOrderService gateway final=false
FieldOrderService notifier final=false
+5
View File
@@ -0,0 +1,5 @@
# The order Spring touches one bean that uses all three styles
constructor : constructor arg=set, field=null, setter=null
setter : setter arg=set, field=set
@PostConstruct: constructor=set, field=set, setter=set
@@ -0,0 +1,6 @@
# A field-injected collaborator used in the constructor
context started: false
top exception : org.springframework.beans.factory.BeanCreationException
top message : Error creating bean with name 'fieldTrapService': Failed to instantiate [com.ankurm.coredi.injection.FieldTrapService]: Constructor threw exception
root cause : java.lang.NullPointerException: Cannot invoke "com.ankurm.coredi.injection.PaymentGateway.charge(String, int)" because "this.gateway" is null
@@ -0,0 +1,14 @@
# Two constructors: which one does Spring pick?
--- TwoConstructorsService: no-arg + one-arg, neither annotated ---
[no-arg constructor used]
gateway injected: false
--- TwoConstructorsNoDefault: one-arg + two-arg, neither annotated ---
org.springframework.beans.factory.BeanCreationException
Error creating bean with name 'twoConstructorsNoDefault': Failed to instantiate [com.ankurm.coredi.injection.TwoConstructorsNoDefault]: No default constructor found
root cause: java.lang.NoSuchMethodException: com.ankurm.coredi.injection.TwoConstructorsNoDefault.<init>()
--- TwoConstructorsAnnotated: @Autowired on one constructor ---
[annotated constructor used]
+4
View File
@@ -0,0 +1,4 @@
# @Autowired(required = false) on a setter
with a Notifier bean : notifier present
without a Notifier bean : no notifier configured, auditing silently
@@ -0,0 +1,5 @@
# Inside a Spring context all three styles work
ConstructorOrderService -> charged 700 cents for B-1
SetterOrderService -> charged 700 cents for B-1
FieldOrderService -> charged 700 cents for B-1
@@ -0,0 +1,20 @@
# Constructor cycle vs field cycle: plain Spring and Spring Boot
--- plain Spring, constructor cycle (CtorA <-> CtorB) ---
started: false
root cause: org.springframework.beans.factory.BeanCurrentlyInCreationException
Error creating bean with name 'ctorA': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
--- plain Spring, field cycle (FieldA <-> FieldB) ---
started: true
FieldA.b is FieldB: true
--- Spring Boot, field cycle, default settings ---
FAILED: BeanCurrentlyInCreationException: Error creating bean with name 'fieldA': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
--- Spring Boot, field cycle, spring.main.allow-circular-references=true ---
started
--- Spring Boot, constructor cycle, spring.main.allow-circular-references=true ---
FAILED: BeanCurrentlyInCreationException: Error creating bean with name 'ctorA': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
@@ -0,0 +1,6 @@
# @Lazy on one constructor parameter breaks a constructor cycle
context started: true
LazyA.b runtime class : com.ankurm.coredi.injection.LazyB$$SpringCGLIB$$0
LazyA.b.hello() : LazyB reached through LazyA's proxy
LazyA.b is the real LazyB bean: false
+8
View File
@@ -0,0 +1,8 @@
# Two PaymentGateway beans and a consumer that asks for one
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
+11
View File
@@ -0,0 +1,11 @@
# Who wins when several rules apply at once? (one mini-context per row)
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
+7
View File
@@ -0,0 +1,7 @@
# No PaymentGateway bean at all
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: {}
@@ -0,0 +1,19 @@
# Five ways to say 'this may not exist', with zero and with two Notifier beans (Email registered before Sms)
--- 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
--- 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
@@ -0,0 +1,5 @@
# List, Map and Set injection; registered Push, Email, Sms in that order
List<Notifier> order : [SmsNotifier, EmailNotifier, PushNotifier]
Map<String,Notifier> : [pushNotifier, emailNotifier, smsNotifier]
Set<Notifier> size : 3
+16
View File
@@ -0,0 +1,16 @@
# A List<Plugin> injection point when zero Plugin beans exist
--- 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: {}
--- ways to make an absent collection legal ---
Optional<List<Plugin>> : isPresent=false
@Nullable List<Plugin> : value=null
ObjectProvider<Plugin> -> list : []
@Autowired(required=false) list: []
@@ -0,0 +1,5 @@
# Generic type arguments and @Resource
List<Handler<String>> : [StringHandler]
Handler<Integer> : IntegerHandler
@Resource : paypalGateway field -> PaypalGateway, whateverIWantToCallIt field -> StripeGateway
+6
View File
@@ -0,0 +1,6 @@
# getDependenciesForBean: who did Spring wire into whom?
constructorOrderService depends on [paymentGateway, notifier]
setterOrderService depends on [paymentGateway, notifier]
fieldOrderService depends on [paymentGateway, notifier]
paymentGateway is used by [constructorOrderService, setterOrderService, fieldOrderService]
+5
View File
@@ -0,0 +1,5 @@
# The parameter-name fallback needs javac -parameters (compiled twice from the same source)
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
@@ -0,0 +1,23 @@
# Boot start-up failure for a constructor cycle: --spring.profiles.active=ctor-cycle
ERROR --- o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| bootCycles.CtorOne defined in URL [jar:nested:target/core-di-1.0.0.jar/!BOOT-INF/classes/!/com/ankurm/coredi/boot/BootCycles$CtorOne.class]
↑ ↓
| bootCycles.CtorTwo defined in URL [jar:nested:target/core-di-1.0.0.jar/!BOOT-INF/classes/!/com/ankurm/coredi/boot/BootCycles$CtorTwo.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
@@ -0,0 +1,23 @@
# Boot start-up failure for a field-injection cycle: --spring.profiles.active=field-cycle
ERROR --- o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| bootCycles.FieldOne (field com.ankurm.coredi.boot.BootCycles$FieldTwo com.ankurm.coredi.boot.BootCycles$FieldOne.two)
↑ ↓
| bootCycles.FieldTwo (field com.ankurm.coredi.boot.BootCycles$FieldOne com.ankurm.coredi.boot.BootCycles$FieldTwo.one)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
@@ -0,0 +1,3 @@
# Same field cycle with --spring.main.allow-circular-references=true
STARTED. Beans from the cycle: [bootCycles.FieldOne, bootCycles.FieldTwo]
+3
View File
@@ -0,0 +1,3 @@
# @Lazy on one constructor parameter: --spring.profiles.active=lazy-cycle
STARTED. Beans from the cycle: [bootCycles.LazyOne, bootCycles.LazyTwo]
@@ -0,0 +1,28 @@
# DefaultListableBeanFactory.determineAutowireCandidate, read with javap
# 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
@@ -0,0 +1,12 @@
# DefaultSingletonBeanRegistry: the fields that hold early references, read with javap
# jar: spring-beans-7.0.9.jar
private final java.util.Map<java.lang.String, java.lang.Object> singletonObjects;
private final java.util.Map<java.lang.String, org.springframework.beans.factory.ObjectFactory<?>> singletonFactories;
private final java.util.Map<java.lang.String, java.lang.Object> earlySingletonObjects;
private final java.util.Set<java.lang.String> singletonsCurrentlyInCreation;
# DefaultListableBeanFactory / AbstractAutowireCapableBeanFactory: the allow-circular-references switch
private boolean allowCircularReferences;
public void setAllowCircularReferences(boolean);
public boolean isAllowCircularReferences();
@@ -0,0 +1,10 @@
# org.springframework.lang.Nullable in spring-core-7.0.9.jar, read with javap -v
#9 = Utf8 Deprecated
#10 = Utf8 RuntimeVisibleAnnotations
#22 = Utf8 Ljava/lang/Deprecated;
#23 = Utf8 since
Deprecated: true
RuntimeVisibleAnnotations:
java.lang.Deprecated(
since="7.0"
@@ -0,0 +1,7 @@
# spring-boot-starter-parent-4.1.1.pom: the compiler flag that parameter-name matching depends on
111- <artifactId>maven-compiler-plugin</artifactId>
112- <configuration>
113: <parameters>true</parameters>
114- </configuration>
115- </plugin>