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:
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user