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:
@@ -31,6 +31,11 @@ files.
|
|||||||
| [`custom-validation/`](custom-validation) | [Custom Validation in Spring Boot: Beyond the Basics!](https://ankurm.com/custom-validation-in-spring-boot-beyond-the-basics/) | the `javax.validation` to `jakarta.validation` namespace fix Boot 3 already required, Jakarta Validation 3.1's record-validation clarification proven on both field- and class-level custom constraints, a record validation failure's empty 400 body by default, and what `spring.mvc.problemdetails.enabled` does and does not fix |
|
| [`custom-validation/`](custom-validation) | [Custom Validation in Spring Boot: Beyond the Basics!](https://ankurm.com/custom-validation-in-spring-boot-beyond-the-basics/) | the `javax.validation` to `jakarta.validation` namespace fix Boot 3 already required, Jakarta Validation 3.1's record-validation clarification proven on both field- and class-level custom constraints, a record validation failure's empty 400 body by default, and what `spring.mvc.problemdetails.enabled` does and does not fix |
|
||||||
| [`etag-caching/`](etag-caching) | [Mastering Cache Control with ETag in Spring Boot RESTful APIs](https://ankurm.com/etag-cache-control-rest-api-spring-boot/) | `spring-boot-starter-web`'s own POM now reading "deprecated in favor of spring-boot-starter-webmvc", `ShallowEtagHeaderFilter` and `WebRequest.checkNotModified()` re-verified unchanged on Spring Framework 7, deep cache vs shallow cache, and conditional `PUT` with `If-Match` as optimistic locking |
|
| [`etag-caching/`](etag-caching) | [Mastering Cache Control with ETag in Spring Boot RESTful APIs](https://ankurm.com/etag-cache-control-rest-api-spring-boot/) | `spring-boot-starter-web`'s own POM now reading "deprecated in favor of spring-boot-starter-webmvc", `ShallowEtagHeaderFilter` and `WebRequest.checkNotModified()` re-verified unchanged on Spring Framework 7, deep cache vs shallow cache, and conditional `PUT` with `If-Match` as optimistic locking |
|
||||||
| [`restclient-basic-auth/`](restclient-basic-auth) | [Spring Boot RestTemplate with Basic Auth: A Modern Guide](https://ankurm.com/spring-boot-resttemplate-with-basic-auth-a-modern-guide/) | RestClient with Basic Auth two ways against a real embedded server, `{noop}` passwords confirmed to emit no runtime warning at all, `spring-boot-starter-restclient` as its own required Boot 4 module, and the `RestClient.exchange()` trap covered in depth by the [RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/) |
|
| [`restclient-basic-auth/`](restclient-basic-auth) | [Spring Boot RestTemplate with Basic Auth: A Modern Guide](https://ankurm.com/spring-boot-resttemplate-with-basic-auth-a-modern-guide/) | RestClient with Basic Auth two ways against a real embedded server, `{noop}` passwords confirmed to emit no runtime warning at all, `spring-boot-starter-restclient` as its own required Boot 4 module, and the `RestClient.exchange()` trap covered in depth by the [RestTemplate to RestClient migration guide](https://ankurm.com/resttemplate-to-restclient-migration-guide/) |
|
||||||
|
| [`core-di/`](core-di) | [Dependency Injection in Spring Boot 4: Constructor vs Setter vs Field (and Why Field Injection Hurts)](https://ankurm.com/spring-boot-4-dependency-injection-constructor-setter-field/) and [@Autowired Explained: By-Type Resolution, @Qualifier, @Primary, ObjectProvider and List Injection](https://ankurm.com/spring-autowired-qualifier-primary-objectprovider-list-injection/) | one `OrderService` in three injection styles built with plain `new`, a field-injected dependency used in a constructor, circular dependencies in plain Spring vs Boot with the real failure report, `@Lazy` injecting a proxy, and the `@Autowired` resolution ladder read from Spring 7.0.9 bytecode (name match beats `@Priority`), plus the empty-collection trap that only bites field and setter injection |
|
||||||
|
|
||||||
|
`core-di` and `core-beans` are the exception: they have **no `docs/` folder**. Their deeper material lives in
|
||||||
|
collapsible sections inside the articles themselves, and their captured output sits in a top-level `output/`
|
||||||
|
directory instead of `docs/output/`.
|
||||||
|
|
||||||
Articles whose text is kept here rather than only on the blog have it under
|
Articles whose text is kept here rather than only on the blog have it under
|
||||||
`<directory>/post/` — `post.md` for the body and `meta.md` for the title, excerpt and
|
`<directory>/post/` — `post.md` for the body and `meta.md` for the title, excerpt and
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
*.class
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# core-di
|
||||||
|
|
||||||
|
Companion project for two articles on **[ankurm.com](https://ankurm.com)**.
|
||||||
|
|
||||||
|
| Article | What it demonstrates |
|
||||||
|
|---|---|
|
||||||
|
| [Dependency Injection in Spring Boot 4: Constructor vs Setter vs Field (and Why Field Injection Hurts)](https://ankurm.com/spring-boot-4-dependency-injection-constructor-setter-field/) | the same service in three injection styles built without Spring, injection order, a field-injected dependency used in a constructor, two-constructor ambiguity, circular dependencies in plain Spring vs Boot, and what `@Lazy` really injects |
|
||||||
|
| [@Autowired Explained: By-Type Resolution, @Qualifier, @Primary, ObjectProvider and List Injection](https://ankurm.com/spring-autowired-qualifier-primary-objectprovider-list-injection/) | the real `NoUniqueBeanDefinitionException`, the resolution ladder read from Spring 7.0.9 bytecode, `Optional` vs `@Nullable` vs `ObjectProvider`, `List`/`Map` injection order, and the empty-collection trap |
|
||||||
|
|
||||||
|
Every console block, exception message and count quoted in those articles came out of `output/`,
|
||||||
|
and every file there is regenerated by one script. Most are written by the test suite, so if a
|
||||||
|
claim stops being true the build goes red.
|
||||||
|
|
||||||
|
There is deliberately **no `docs/` folder**: the deeper material lives in collapsible "going
|
||||||
|
deeper" sections inside the articles themselves, next to the paragraph each one extends.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Spring Boot | 4.1.1 |
|
||||||
|
| Spring Framework | 7.0.9 |
|
||||||
|
| JDK | 25 (Temurin 25.0.4.1+1) |
|
||||||
|
| Maven | 3.9 |
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=/path/to/jdk-25
|
||||||
|
mvn test # runs the scenarios and rewrites the test-written files in output/
|
||||||
|
./scripts/run-all.sh # everything, including the script-captured files
|
||||||
|
```
|
||||||
|
|
||||||
|
Reproduce the Boot start-up failure report by hand:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -DskipTests package
|
||||||
|
java -jar target/core-di-1.0.0.jar --spring.profiles.active=ctor-cycle
|
||||||
|
java -jar target/core-di-1.0.0.jar --spring.profiles.active=field-cycle
|
||||||
|
java -jar target/core-di-1.0.0.jar --spring.profiles.active=field-cycle --spring.main.allow-circular-references=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source layout
|
||||||
|
|
||||||
|
| Package | What it holds |
|
||||||
|
|---|---|
|
||||||
|
| `injection/` | one `OrderService` written three ways (constructor, setter, field), the trap classes (field used in a constructor, two constructors, circular pairs, `@Lazy`) |
|
||||||
|
| `resolution/` | two gateways and every way to choose between them (`@Primary`, `@Qualifier`, name, `@Priority`, `@Fallback`, custom qualifier), optional injection, collections, generics, `@Resource` |
|
||||||
|
| `boot/` | profile-gated cycle beans for the real "APPLICATION FAILED TO START" report |
|
||||||
|
|
||||||
|
## Captured output
|
||||||
|
|
||||||
|
Files 01-18 (tests) and 19-22 (`capture-failure-analysis.sh`), 23-26 (`capture-bytecode.sh`). Timing rows assert coarse thresholds, not exact milliseconds; treat them as indicative.
|
||||||
|
|
||||||
|
| File | What it shows |
|
||||||
|
|---|---|
|
||||||
|
| [`01-three-styles-under-plain-new.txt`](output/01-three-styles-under-plain-new.txt) | The same OrderService built with plain 'new', no Spring anywhere |
|
||||||
|
| [`02-fields-and-finality.txt`](output/02-fields-and-finality.txt) | Which injected fields can be final? (reflection over the three variants) |
|
||||||
|
| [`03-injection-order.txt`](output/03-injection-order.txt) | The order Spring touches one bean that uses all three styles |
|
||||||
|
| [`04-field-used-in-constructor.txt`](output/04-field-used-in-constructor.txt) | A field-injected collaborator used in the constructor |
|
||||||
|
| [`05-multiple-constructors.txt`](output/05-multiple-constructors.txt) | Two constructors: which one does Spring pick? |
|
||||||
|
| [`06-optional-setter.txt`](output/06-optional-setter.txt) | @Autowired(required = false) on a setter |
|
||||||
|
| [`07-spring-wires-all-three.txt`](output/07-spring-wires-all-three.txt) | Inside a Spring context all three styles work |
|
||||||
|
| [`08-circular-plain-vs-boot.txt`](output/08-circular-plain-vs-boot.txt) | Constructor cycle vs field cycle: plain Spring and Spring Boot |
|
||||||
|
| [`09-lazy-breaks-constructor-cycle.txt`](output/09-lazy-breaks-constructor-cycle.txt) | @Lazy on one constructor parameter breaks a constructor cycle |
|
||||||
|
| [`10-no-unique-bean.txt`](output/10-no-unique-bean.txt) | Two PaymentGateway beans and a consumer that asks for one |
|
||||||
|
| [`11-resolution-ladder.txt`](output/11-resolution-ladder.txt) | Who wins when several rules apply at once? (one mini-context per row) |
|
||||||
|
| [`12-missing-bean.txt`](output/12-missing-bean.txt) | No PaymentGateway bean at all |
|
||||||
|
| [`13-optional-and-objectprovider.txt`](output/13-optional-and-objectprovider.txt) | Five ways to say 'this may not exist', with zero and with two Notifier beans (Email registered before Sms) |
|
||||||
|
| [`14-list-map-set-injection.txt`](output/14-list-map-set-injection.txt) | List, Map and Set injection; registered Push, Email, Sms in that order |
|
||||||
|
| [`15-empty-collection.txt`](output/15-empty-collection.txt) | A List<Plugin> injection point when zero Plugin beans exist |
|
||||||
|
| [`16-generics-and-resource.txt`](output/16-generics-and-resource.txt) | Generic type arguments and @Resource |
|
||||||
|
| [`17-dependency-graph.txt`](output/17-dependency-graph.txt) | getDependenciesForBean: who did Spring wire into whom? |
|
||||||
|
| [`18-parameters-flag.txt`](output/18-parameters-flag.txt) | The parameter-name fallback needs javac -parameters (compiled twice from the same source) |
|
||||||
|
| [`19-boot-failure-analysis-ctor-cycle.txt`](output/19-boot-failure-analysis-ctor-cycle.txt) | Boot start-up failure for a constructor cycle: --spring.profiles.active=ctor-cycle |
|
||||||
|
| [`20-boot-failure-analysis-field-cycle.txt`](output/20-boot-failure-analysis-field-cycle.txt) | Boot start-up failure for a field-injection cycle: --spring.profiles.active=field-cycle |
|
||||||
|
| [`21-boot-allow-circular-references.txt`](output/21-boot-allow-circular-references.txt) | Same field cycle with --spring.main.allow-circular-references=true |
|
||||||
|
| [`22-boot-lazy-cycle.txt`](output/22-boot-lazy-cycle.txt) | @Lazy on one constructor parameter: --spring.profiles.active=lazy-cycle |
|
||||||
|
| [`23-determine-autowire-candidate-bytecode.txt`](output/23-determine-autowire-candidate-bytecode.txt) | DefaultListableBeanFactory.determineAutowireCandidate, read with javap |
|
||||||
|
| [`24-early-reference-caches.txt`](output/24-early-reference-caches.txt) | DefaultSingletonBeanRegistry: the fields that hold early references, read with javap |
|
||||||
|
| [`25-spring-nullable-deprecation.txt`](output/25-spring-nullable-deprecation.txt) | org.springframework.lang.Nullable in spring-core-7.0.9.jar, read with javap -v |
|
||||||
|
| [`26-boot-parent-parameters-flag.txt`](output/26-boot-parent-parameters-flag.txt) | spring-boot-starter-parent-4.1.1.pom: the compiler flag that parameter-name matching depends on |
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
MIT, see the repository root.
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.1.1</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>core-di</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>core-di</name>
|
||||||
|
<description>Dependency injection in Spring Boot 4: constructor vs setter vs field, and how @Autowired resolves candidates</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Reads the resolution order out of Spring's own bytecode instead of trusting the reference
|
||||||
|
# documentation's prose: which determine* helpers DefaultListableBeanFactory calls, in which order.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mkdir -p output target/bytecode
|
||||||
|
JAR=$(find ~/.m2/repository/org/springframework/spring-beans -name 'spring-beans-*.jar' ! -name '*sources*' | sort | tail -1)
|
||||||
|
unzip -o -q "$JAR" 'org/springframework/beans/factory/support/DefaultListableBeanFactory*' -d target/bytecode
|
||||||
|
{
|
||||||
|
echo "# DefaultListableBeanFactory.determineAutowireCandidate, read with javap"
|
||||||
|
echo "# jar: $(basename "$JAR")"
|
||||||
|
echo
|
||||||
|
javap -p -c -cp target/bytecode org.springframework.beans.factory.support.DefaultListableBeanFactory 2>&1 \
|
||||||
|
| grep -v 'Picked up' \
|
||||||
|
| awk '/protected java.lang.String determineAutowireCandidate\(/{f=1} f{print} /^$/{if(f) exit}' \
|
||||||
|
| grep -E 'determineAutowireCandidate|invoke|instanceof' | sed 's/^ *[0-9]*: //'
|
||||||
|
} > output/23-determine-autowire-candidate-bytecode.txt
|
||||||
|
cat output/23-determine-autowire-candidate-bytecode.txt
|
||||||
|
|
||||||
|
# --- the early-reference caches behind circular-reference support
|
||||||
|
JAR2=$(find ~/.m2/repository/org/springframework/spring-beans -name 'spring-beans-*.jar' ! -name '*sources*' | sort | tail -1)
|
||||||
|
unzip -o -q "$JAR2" 'org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.class' -d target/bytecode
|
||||||
|
{
|
||||||
|
echo "# DefaultSingletonBeanRegistry: the fields that hold early references, read with javap"
|
||||||
|
echo "# jar: $(basename "$JAR2")"
|
||||||
|
echo
|
||||||
|
javap -p -cp target/bytecode org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 2>&1 \
|
||||||
|
| grep -v 'Picked up' | grep -E ' (singletonObjects|earlySingletonObjects|singletonFactories|singletonsCurrentlyInCreation|allowCircularReferences)[;]?' || true
|
||||||
|
echo
|
||||||
|
echo "# DefaultListableBeanFactory / AbstractAutowireCapableBeanFactory: the allow-circular-references switch"
|
||||||
|
unzip -o -q "$JAR2" 'org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.class' -d target/bytecode
|
||||||
|
javap -p -cp target/bytecode org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 2>&1 \
|
||||||
|
| grep -v 'Picked up' | grep -i 'allowCircularReferences' || true
|
||||||
|
} > output/24-early-reference-caches.txt
|
||||||
|
cat output/24-early-reference-caches.txt
|
||||||
|
|
||||||
|
# --- is the old Spring-specific @Nullable deprecated in 7.0.9?
|
||||||
|
JAR3=$(find ~/.m2/repository/org/springframework/spring-core -name 'spring-core-7*.jar' ! -name '*sources*' | sort | tail -1)
|
||||||
|
unzip -o -q "$JAR3" 'org/springframework/lang/Nullable.class' -d target/bytecode
|
||||||
|
{
|
||||||
|
echo "# org.springframework.lang.Nullable in $(basename "$JAR3"), read with javap -v"
|
||||||
|
echo
|
||||||
|
javap -v -cp target/bytecode org.springframework.lang.Nullable 2>&1 | grep -v 'Picked up' | grep -E 'Deprecated|RuntimeVisibleAnnotations|Ljava/lang/Deprecated|forRemoval|since' || true
|
||||||
|
} > output/25-spring-nullable-deprecation.txt
|
||||||
|
cat output/25-spring-nullable-deprecation.txt
|
||||||
|
|
||||||
|
# --- does the Boot parent compile with -parameters?
|
||||||
|
PARENT=$(find ~/.m2/repository/org/springframework/boot/spring-boot-starter-parent -name '*.pom' | sort | tail -1)
|
||||||
|
{
|
||||||
|
echo "# $(basename "$PARENT"): the compiler flag that parameter-name matching depends on"
|
||||||
|
echo
|
||||||
|
grep -n -B2 -A2 '<parameters>' "$PARENT" || echo "(no <parameters> element found)"
|
||||||
|
} > output/26-boot-parent-parameters-flag.txt
|
||||||
|
cat output/26-boot-parent-parameters-flag.txt
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Starts the real Boot application with three different profiles and captures what a developer
|
||||||
|
# actually sees: the "APPLICATION FAILED TO START" report for a circular dependency, and the same
|
||||||
|
# application starting once spring.main.allow-circular-references is switched on.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mkdir -p output
|
||||||
|
JAR=$(ls target/core-di-*.jar | head -1)
|
||||||
|
|
||||||
|
# Drops JAVA_TOOL_OPTIONS noise, machine-specific paths, timestamps and PIDs so reruns diff cleanly.
|
||||||
|
strip() {
|
||||||
|
grep -v -E '^Picked up JAVA_TOOL_OPTIONS' \
|
||||||
|
| sed -E 's#jar:nested:[^ ]*/target/#jar:nested:target/#; s#^[0-9T:.+-]+ (ERROR|WARN|INFO) [0-9]+ --- \[core-di\] \[[^]]*\] #\1 --- #' \
|
||||||
|
|| true
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Boot start-up failure for a constructor cycle: --spring.profiles.active=ctor-cycle"
|
||||||
|
echo
|
||||||
|
java -jar "$JAR" --spring.profiles.active=ctor-cycle --spring.main.banner-mode=off --logging.level.root=ERROR 2>&1 | strip || true
|
||||||
|
} > output/19-boot-failure-analysis-ctor-cycle.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Boot start-up failure for a field-injection cycle: --spring.profiles.active=field-cycle"
|
||||||
|
echo
|
||||||
|
java -jar "$JAR" --spring.profiles.active=field-cycle --spring.main.banner-mode=off --logging.level.root=ERROR 2>&1 | strip || true
|
||||||
|
} > output/20-boot-failure-analysis-field-cycle.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Same field cycle with --spring.main.allow-circular-references=true"
|
||||||
|
echo
|
||||||
|
java -jar "$JAR" --spring.profiles.active=field-cycle --spring.main.allow-circular-references=true \
|
||||||
|
--spring.main.banner-mode=off --logging.level.root=WARN 2>&1 | strip
|
||||||
|
} > output/21-boot-allow-circular-references.txt
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# @Lazy on one constructor parameter: --spring.profiles.active=lazy-cycle"
|
||||||
|
echo
|
||||||
|
java -jar "$JAR" --spring.profiles.active=lazy-cycle --spring.main.banner-mode=off --logging.level.root=WARN 2>&1 | strip
|
||||||
|
} > output/22-boot-lazy-cycle.txt
|
||||||
|
|
||||||
|
cat output/19-*.txt output/20-*.txt output/21-*.txt output/22-*.txt
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file under output/.
|
||||||
|
#
|
||||||
|
# ./scripts/run-all.sh
|
||||||
|
#
|
||||||
|
# Needs a JDK 25 and Maven 3.9. Transcripts 01-18 come out of the test suite, which is the point:
|
||||||
|
# the figures in the two articles are assertions that fail the build if they stop being true.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "== test suite (transcripts 01-18)"
|
||||||
|
mvn -B test
|
||||||
|
|
||||||
|
echo "== package the runnable Boot app"
|
||||||
|
mvn -B -q -DskipTests package
|
||||||
|
|
||||||
|
echo "== Boot start-up failure reports (19-22)"
|
||||||
|
./scripts/capture-failure-analysis.sh
|
||||||
|
|
||||||
|
echo "== bytecode and pom facts (23-26)"
|
||||||
|
./scripts/capture-bytecode.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "output:"
|
||||||
|
ls -1 output
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only used by scripts/capture-failure-analysis.sh: it starts a real Boot application so the
|
||||||
|
* "APPLICATION FAILED TO START" report for a circular dependency can be captured verbatim.
|
||||||
|
* Everything else in this module runs from the tests.
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = "com.ankurm.coredi.boot")
|
||||||
|
public class DiApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
var ctx = SpringApplication.run(DiApplication.class, args);
|
||||||
|
var cycleBeans = java.util.Arrays.stream(ctx.getBeanDefinitionNames())
|
||||||
|
.filter(n -> n.startsWith("bootCycles.")).sorted().toList();
|
||||||
|
System.out.println("STARTED. Beans from the cycle: " + cycleBeans);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.ankurm.coredi.boot;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Three ways to start the same application with a cycle in it. Pick with --spring.profiles.active. */
|
||||||
|
public final class BootCycles {
|
||||||
|
|
||||||
|
private BootCycles() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("ctor-cycle")
|
||||||
|
public static class CtorOne {
|
||||||
|
public CtorOne(CtorTwo two) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("ctor-cycle")
|
||||||
|
public static class CtorTwo {
|
||||||
|
public CtorTwo(CtorOne one) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("field-cycle")
|
||||||
|
public static class FieldOne {
|
||||||
|
@Autowired
|
||||||
|
FieldTwo two;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("field-cycle")
|
||||||
|
public static class FieldTwo {
|
||||||
|
@Autowired
|
||||||
|
FieldOne one;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("lazy-cycle")
|
||||||
|
public static class LazyOne {
|
||||||
|
public LazyOne(@Lazy LazyTwo two) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("lazy-cycle")
|
||||||
|
public static class LazyTwo {
|
||||||
|
public LazyTwo(LazyOne one) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** One bean using all three styles at once, only to make Spring's order of operations visible. */
|
||||||
|
public class AllThreeStyles {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Notifier fieldDependency;
|
||||||
|
|
||||||
|
private PaymentGateway setterDependency;
|
||||||
|
|
||||||
|
private final Notifier constructorDependency;
|
||||||
|
|
||||||
|
public AllThreeStyles(Notifier constructorDependency) {
|
||||||
|
this.constructorDependency = constructorDependency;
|
||||||
|
Trace.log("constructor : constructor arg=" + describe(constructorDependency)
|
||||||
|
+ ", field=" + describe(fieldDependency) + ", setter=" + describe(setterDependency));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setSetterDependency(PaymentGateway gateway) {
|
||||||
|
this.setterDependency = gateway;
|
||||||
|
Trace.log("setter : setter arg=" + describe(gateway) + ", field=" + describe(fieldDependency));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void afterInjection() {
|
||||||
|
Trace.log("@PostConstruct: constructor=" + describe(constructorDependency)
|
||||||
|
+ ", field=" + describe(fieldDependency) + ", setter=" + describe(setterDependency));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String describe(Object o) {
|
||||||
|
return o == null ? "null" : "set";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/** The two collaborators, registered once so every service variant can be pointed at them. */
|
||||||
|
@Configuration
|
||||||
|
public class BaseBeans {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
PaymentGateway paymentGateway() {
|
||||||
|
return new LoggingPaymentGateway();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Notifier notifier() {
|
||||||
|
return new RecordingNotifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor injection. One constructor, so {@code @Autowired} is not needed. Both fields are
|
||||||
|
* {@code final}, and the class cannot be instantiated without its collaborators.
|
||||||
|
*/
|
||||||
|
public class ConstructorOrderService implements OrderService {
|
||||||
|
|
||||||
|
private final PaymentGateway gateway;
|
||||||
|
private final Notifier notifier;
|
||||||
|
|
||||||
|
public ConstructorOrderService(PaymentGateway gateway, Notifier notifier) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String place(String orderId, int cents) {
|
||||||
|
String receipt = gateway.charge(orderId, cents);
|
||||||
|
notifier.send(receipt);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
public class CtorA {
|
||||||
|
public CtorA(CtorB b) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
public class CtorB {
|
||||||
|
public CtorB(CtorA a) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
public class FieldA {
|
||||||
|
@Autowired
|
||||||
|
public FieldB b;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
public class FieldB {
|
||||||
|
@Autowired
|
||||||
|
public FieldA a;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** Field injection. Shortest to write; the only way to supply the fields outside Spring is reflection. */
|
||||||
|
public class FieldOrderService implements OrderService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PaymentGateway gateway;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Notifier notifier;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String place(String orderId, int cents) {
|
||||||
|
String receipt = gateway.charge(orderId, cents);
|
||||||
|
notifier.send(receipt);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** Uses a field-injected collaborator in its constructor, before Spring has had a chance to set it. */
|
||||||
|
public class FieldTrapService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PaymentGateway gateway;
|
||||||
|
|
||||||
|
private final String warmup;
|
||||||
|
|
||||||
|
public FieldTrapService() {
|
||||||
|
this.warmup = gateway.charge("warm-up", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String warmup() {
|
||||||
|
return warmup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
|
/** The cycle is still there; {@code @Lazy} turns one edge into a proxy that resolves on first use. */
|
||||||
|
public class LazyA {
|
||||||
|
|
||||||
|
public final LazyB b;
|
||||||
|
|
||||||
|
public LazyA(@Lazy LazyB b) {
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
public class LazyB {
|
||||||
|
|
||||||
|
final LazyA a;
|
||||||
|
|
||||||
|
public LazyB(LazyA a) {
|
||||||
|
this.a = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hello() {
|
||||||
|
return "LazyB reached through LazyA's proxy";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LoggingPaymentGateway implements PaymentGateway {
|
||||||
|
|
||||||
|
private final List<String> charges = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String charge(String orderId, int cents) {
|
||||||
|
String receipt = "charged " + cents + " cents for " + orderId;
|
||||||
|
charges.add(receipt);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> charges() {
|
||||||
|
return charges;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Notifier {
|
||||||
|
|
||||||
|
void send(String message);
|
||||||
|
|
||||||
|
List<String> sent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** Setter injection's honest use: a dependency that is allowed to be absent. */
|
||||||
|
public class OptionalAudit {
|
||||||
|
|
||||||
|
private Notifier notifier;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setNotifier(Notifier notifier) {
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String status() {
|
||||||
|
return notifier == null ? "no notifier configured, auditing silently" : "notifier present";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
public interface OrderService {
|
||||||
|
|
||||||
|
/** Charges the customer, then tells them. The order matters: see the setter and field variants. */
|
||||||
|
String place(String orderId, int cents);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
/** The collaborator every order service needs. */
|
||||||
|
public interface PaymentGateway {
|
||||||
|
|
||||||
|
String charge(String orderId, int cents);
|
||||||
|
|
||||||
|
/** Every charge that really happened, so a test can prove a half-built service still took money. */
|
||||||
|
java.util.List<String> charges();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RecordingNotifier implements Notifier {
|
||||||
|
|
||||||
|
private final List<String> sent = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String message) {
|
||||||
|
sent.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> sent() {
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** Setter injection. The object exists, half-built, before Spring calls the setters. */
|
||||||
|
public class SetterOrderService implements OrderService {
|
||||||
|
|
||||||
|
private PaymentGateway gateway;
|
||||||
|
private Notifier notifier;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setGateway(PaymentGateway gateway) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setNotifier(Notifier notifier) {
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String place(String orderId, int cents) {
|
||||||
|
String receipt = gateway.charge(orderId, cents);
|
||||||
|
notifier.send(receipt);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** A shared event log so a test can print the order in which Spring touches a bean. */
|
||||||
|
public final class Trace {
|
||||||
|
|
||||||
|
private static final List<String> EVENTS = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
private Trace() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void log(String event) {
|
||||||
|
EVENTS.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> drain() {
|
||||||
|
List<String> copy;
|
||||||
|
synchronized (EVENTS) {
|
||||||
|
copy = new ArrayList<>(EVENTS);
|
||||||
|
EVENTS.clear();
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/** The fix: say which constructor Spring should use. */
|
||||||
|
public class TwoConstructorsAnnotated {
|
||||||
|
|
||||||
|
private final PaymentGateway gateway;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TwoConstructorsAnnotated(PaymentGateway gateway) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
Trace.log("annotated constructor used");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwoConstructorsAnnotated(PaymentGateway gateway, Notifier notifier) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
Trace.log("two-arg constructor used");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
/** Two constructors, no no-argument one, none marked {@code @Autowired}: Spring cannot choose. */
|
||||||
|
public class TwoConstructorsNoDefault {
|
||||||
|
|
||||||
|
public TwoConstructorsNoDefault(PaymentGateway gateway) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwoConstructorsNoDefault(PaymentGateway gateway, Notifier notifier) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ankurm.coredi.injection;
|
||||||
|
|
||||||
|
/** Two public constructors, none marked {@code @Autowired}: Spring falls back to the no-argument one. */
|
||||||
|
public class TwoConstructorsService {
|
||||||
|
|
||||||
|
private PaymentGateway gateway;
|
||||||
|
|
||||||
|
public TwoConstructorsService() {
|
||||||
|
Trace.log("no-arg constructor used");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwoConstructorsService(PaymentGateway gateway) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
Trace.log("one-arg constructor used");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasGateway() {
|
||||||
|
return gateway != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.Notifier;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
|
||||||
|
public class CheckoutFast {
|
||||||
|
|
||||||
|
private final PaymentGateway gateway;
|
||||||
|
|
||||||
|
public CheckoutFast(@Fast PaymentGateway gateway) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String gatewayClass() {
|
||||||
|
return gateway.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
|
||||||
|
/** 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
|
||||||
|
/** 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
|
||||||
|
public class CheckoutQualified {
|
||||||
|
|
||||||
|
private final PaymentGateway gateway;
|
||||||
|
|
||||||
|
public CheckoutQualified(@Qualifier("paypalGateway") PaymentGateway gateway) {
|
||||||
|
this.gateway = gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String gatewayClass() {
|
||||||
|
return gateway.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.RecordingNotifier;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
|
@Order(2)
|
||||||
|
public class EmailNotifier extends RecordingNotifier {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Fallback;
|
||||||
|
|
||||||
|
/** Wins only when nothing else qualifies. */
|
||||||
|
@Fallback
|
||||||
|
public class FallbackGateway extends PaypalGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** 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 {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
@Fast
|
||||||
|
public class FastGateway extends StripeGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** The generic type argument is part of the injection point: only Handler<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
public interface Handler<T> {
|
||||||
|
|
||||||
|
String handle(T input);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
public class IntegerHandler implements Handler<Integer> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String handle(Integer input) {
|
||||||
|
return "integer:" + input;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.Notifier;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
/** Every way to say "this dependency may not exist", one nested class each. */
|
||||||
|
public final class OptionalConsumers {
|
||||||
|
|
||||||
|
private OptionalConsumers() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Required {
|
||||||
|
public Required(Notifier notifier) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithOptional {
|
||||||
|
public final Optional<Notifier> notifier;
|
||||||
|
|
||||||
|
public WithOptional(Optional<Notifier> notifier) {
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithNullable {
|
||||||
|
public final Notifier notifier;
|
||||||
|
|
||||||
|
public WithNullable(@Nullable Notifier notifier) {
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithProvider {
|
||||||
|
public final ObjectProvider<Notifier> provider;
|
||||||
|
|
||||||
|
public WithProvider(ObjectProvider<Notifier> provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithRequiredFalse {
|
||||||
|
public Notifier notifier;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setNotifier(Notifier notifier) {
|
||||||
|
this.notifier = notifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.LoggingPaymentGateway;
|
||||||
|
|
||||||
|
public class PaypalGateway extends LoggingPaymentGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
public interface Plugin {
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
/** What a collection injection point does when zero beans of the element type exist. */
|
||||||
|
public final class PluginHosts {
|
||||||
|
|
||||||
|
private PluginHosts() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Plain {
|
||||||
|
public Plain(List<Plugin> plugins) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same collection, but injected into a field: no constructor "empty collection" fallback applies. */
|
||||||
|
public static class FieldInjected {
|
||||||
|
@Autowired
|
||||||
|
public List<Plugin> plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same collection, injected through a required setter. */
|
||||||
|
public static class SetterInjected {
|
||||||
|
public List<Plugin> plugins;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setPlugins(List<Plugin> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Two constructors, one marked: the "single constructor" fallback for empty collections does not apply. */
|
||||||
|
public static class TwoConstructors {
|
||||||
|
public List<Plugin> plugins;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TwoConstructors(List<Plugin> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TwoConstructors() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithOptional {
|
||||||
|
public final Optional<List<Plugin>> plugins;
|
||||||
|
|
||||||
|
public WithOptional(Optional<List<Plugin>> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithNullable {
|
||||||
|
public final List<Plugin> plugins;
|
||||||
|
|
||||||
|
public WithNullable(@Nullable List<Plugin> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithProvider {
|
||||||
|
public final List<Plugin> plugins;
|
||||||
|
|
||||||
|
public WithProvider(ObjectProvider<Plugin> provider) {
|
||||||
|
this.plugins = provider.orderedStream().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WithRequiredFalse {
|
||||||
|
public List<Plugin> plugins = List.of();
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
public void setPlugins(List<Plugin> plugins) {
|
||||||
|
this.plugins = plugins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
@Primary
|
||||||
|
public class PrimaryStripeGateway extends StripeGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import jakarta.annotation.Priority;
|
||||||
|
|
||||||
|
@Priority(1)
|
||||||
|
public class PriorityOneGateway extends StripeGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import jakarta.annotation.Priority;
|
||||||
|
|
||||||
|
@Priority(2)
|
||||||
|
public class PriorityTwoGateway extends PaypalGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.RecordingNotifier;
|
||||||
|
|
||||||
|
/** No {@code @Order}: sorts after every ordered bean. */
|
||||||
|
public class PushNotifier extends RecordingNotifier {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
|
/** {@code @Resource} matches by name first; the field name is the default name. */
|
||||||
|
public class ResourceConsumer {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
PaymentGateway paypalGateway;
|
||||||
|
|
||||||
|
@Resource(name = "stripeGateway")
|
||||||
|
PaymentGateway whateverIWantToCallIt;
|
||||||
|
|
||||||
|
public String describe() {
|
||||||
|
return "paypalGateway field -> " + paypalGateway.getClass().getSimpleName()
|
||||||
|
+ ", whateverIWantToCallIt field -> " + whateverIWantToCallIt.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.RecordingNotifier;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
|
@Order(1)
|
||||||
|
public class SmsNotifier extends RecordingNotifier {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
public class StringHandler implements Handler<String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String handle(String input) {
|
||||||
|
return "string:" + input;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ankurm.coredi.resolution;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.LoggingPaymentGateway;
|
||||||
|
|
||||||
|
public class StripeGateway extends LoggingPaymentGateway {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: core-di
|
||||||
|
main:
|
||||||
|
banner-mode: off
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.*;
|
||||||
|
import com.ankurm.coredi.resolution.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
|
||||||
|
/** Post 17: how @Autowired chooses between candidates. */
|
||||||
|
class CandidateResolutionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void twoCandidatesNoHint() {
|
||||||
|
try (var t = new Transcript("10-no-unique-bean.txt", "Two PaymentGateway beans and a consumer that asks for one")) {
|
||||||
|
var outcome = Ctx.tryStart(StripeGateway.class, PaypalGateway.class, CheckoutPlain.class);
|
||||||
|
assertThat(outcome.started()).isFalse();
|
||||||
|
t.line("started: false");
|
||||||
|
t.line("top exception : %s", outcome.failure().getClass().getName());
|
||||||
|
t.line("top message : %s", outcome.failure().getMessage());
|
||||||
|
t.blank();
|
||||||
|
t.line("root cause : %s", Ctx.root(outcome.failure()).getClass().getName());
|
||||||
|
t.line("root message : %s", Ctx.root(outcome.failure()).getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theResolutionLadder() {
|
||||||
|
try (var t = new Transcript("11-resolution-ladder.txt",
|
||||||
|
"Who wins when several rules apply at once? (one mini-context per row)")) {
|
||||||
|
t.line("%-58s %s", "beans registered / injection point", "winner");
|
||||||
|
t.line("%-58s %s", "-".repeat(58), "-".repeat(20));
|
||||||
|
|
||||||
|
row(t, "@Primary Stripe + Paypal; CheckoutPlain", PrimaryStripeGateway.class, PaypalGateway.class,
|
||||||
|
CheckoutPlain.class);
|
||||||
|
row(t, "@Primary Stripe + Paypal; @Qualifier(\"paypalGateway\")", PrimaryStripeGateway.class,
|
||||||
|
PaypalGateway.class, CheckoutQualified.class);
|
||||||
|
row(t, "@Priority(1) Stripe + @Priority(2) Paypal; CheckoutPlain", PriorityOneGateway.class,
|
||||||
|
PriorityTwoGateway.class, CheckoutPlain.class);
|
||||||
|
row(t, "@Priority(1) + Paypal(no priority); param named paypalGateway", PriorityOneGateway.class,
|
||||||
|
PaypalGateway.class, CheckoutNamed.class);
|
||||||
|
row(t, "Stripe + Paypal (plain); param named paypalGateway", StripeGateway.class, PaypalGateway.class,
|
||||||
|
CheckoutNamed.class);
|
||||||
|
row(t, "Stripe (plain) + @Fallback Paypal; CheckoutPlain", StripeGateway.class, FallbackGateway.class,
|
||||||
|
CheckoutPlain.class);
|
||||||
|
row(t, "@Fast custom qualifier, FastGateway + Paypal; @Fast param", FastGateway.class, PaypalGateway.class,
|
||||||
|
CheckoutFast.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(Transcript t, String label, Class<?>... classes) {
|
||||||
|
var outcome = Ctx.tryStart(classes);
|
||||||
|
if (outcome.started()) {
|
||||||
|
Object consumer = null;
|
||||||
|
for (Class<?> c : classes) {
|
||||||
|
if (c.getSimpleName().startsWith("Checkout")) {
|
||||||
|
consumer = outcome.context().getBean(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String winner;
|
||||||
|
try {
|
||||||
|
winner = (String) consumer.getClass().getMethod("gatewayClass").invoke(consumer);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
t.line("%-58s %s", label, winner);
|
||||||
|
outcome.closeQuietly();
|
||||||
|
} else {
|
||||||
|
t.line("%-58s FAILED %s", label, Ctx.root(outcome.failure()).getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parameterNameFallbackNeedsTheParametersFlag() throws Exception {
|
||||||
|
try (var t = new Transcript("18-parameters-flag.txt",
|
||||||
|
"The parameter-name fallback needs javac -parameters (compiled twice from the same source)")) {
|
||||||
|
String source = """
|
||||||
|
package demo;
|
||||||
|
import com.ankurm.coredi.injection.PaymentGateway;
|
||||||
|
public class NamedConsumer {
|
||||||
|
private final PaymentGateway paypalGateway;
|
||||||
|
public NamedConsumer(PaymentGateway paypalGateway) { this.paypalGateway = paypalGateway; }
|
||||||
|
public String picked() { return paypalGateway.getClass().getSimpleName(); }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
for (boolean flag : new boolean[] {true, false}) {
|
||||||
|
java.nio.file.Path dir = java.nio.file.Files.createTempDirectory("named");
|
||||||
|
java.nio.file.Path src = dir.resolve("demo/NamedConsumer.java");
|
||||||
|
java.nio.file.Files.createDirectories(src.getParent());
|
||||||
|
java.nio.file.Files.writeString(src, source);
|
||||||
|
var compiler = javax.tools.ToolProvider.getSystemJavaCompiler();
|
||||||
|
java.util.List<String> args = new java.util.ArrayList<>(java.util.List.of(
|
||||||
|
"-cp", System.getProperty("java.class.path"), "-d", dir.toString()));
|
||||||
|
if (flag) {
|
||||||
|
args.add("-parameters");
|
||||||
|
}
|
||||||
|
args.add(src.toString());
|
||||||
|
assertThat(compiler.run(null, null, null, args.toArray(String[]::new))).isZero();
|
||||||
|
try (var loader = new java.net.URLClassLoader(new java.net.URL[] {dir.toUri().toURL()},
|
||||||
|
getClass().getClassLoader())) {
|
||||||
|
Class<?> consumer = loader.loadClass("demo.NamedConsumer");
|
||||||
|
var outcome = Ctx.tryStart(StripeGateway.class, PaypalGateway.class, consumer);
|
||||||
|
if (outcome.started()) {
|
||||||
|
Object bean = outcome.context().getBean(consumer);
|
||||||
|
t.line("javac %-12s : started, picked %s", flag ? "-parameters" : "(no flag)",
|
||||||
|
consumer.getMethod("picked").invoke(bean));
|
||||||
|
outcome.closeQuietly();
|
||||||
|
} else {
|
||||||
|
t.line("javac %-12s : FAILED %s", flag ? "-parameters" : "(no flag)",
|
||||||
|
Ctx.root(outcome.failure()).getClass().getSimpleName());
|
||||||
|
t.line(" %s", Ctx.root(outcome.failure()).getMessage());
|
||||||
|
}
|
||||||
|
assertThat(outcome.started()).isEqualTo(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingBean() {
|
||||||
|
try (var t = new Transcript("12-missing-bean.txt", "No PaymentGateway bean at all")) {
|
||||||
|
var outcome = Ctx.tryStart(CheckoutPlain.class);
|
||||||
|
assertThat(outcome.started()).isFalse();
|
||||||
|
t.line("top exception : %s", outcome.failure().getClass().getName());
|
||||||
|
t.line("top message : %s", outcome.failure().getMessage());
|
||||||
|
t.blank();
|
||||||
|
t.line("root cause : %s", Ctx.root(outcome.failure()).getClass().getName());
|
||||||
|
t.line("root message : %s", Ctx.root(outcome.failure()).getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optionalWaysToAbsent() {
|
||||||
|
try (var t = new Transcript("13-optional-and-objectprovider.txt",
|
||||||
|
"Five ways to say 'this may not exist', with zero and with two Notifier beans (Email registered before Sms)")) {
|
||||||
|
t.section("zero Notifier beans");
|
||||||
|
var required = Ctx.tryStart(OptionalConsumers.Required.class);
|
||||||
|
t.line("plain constructor param : %s", required.started() ? "started" : "FAILED " + Ctx.root(required.failure()).getClass().getSimpleName());
|
||||||
|
var opt = Ctx.plain(OptionalConsumers.WithOptional.class);
|
||||||
|
t.line("Optional<Notifier> : isPresent=%s", opt.getBean(OptionalConsumers.WithOptional.class).notifier.isPresent());
|
||||||
|
var nul = Ctx.plain(OptionalConsumers.WithNullable.class);
|
||||||
|
t.line("@Nullable Notifier : value=%s", nul.getBean(OptionalConsumers.WithNullable.class).notifier);
|
||||||
|
var prov = Ctx.plain(OptionalConsumers.WithProvider.class);
|
||||||
|
var p = prov.getBean(OptionalConsumers.WithProvider.class).provider;
|
||||||
|
t.line("ObjectProvider.getIfAvailable() : %s", p.getIfAvailable());
|
||||||
|
t.line("ObjectProvider.getIfUnique() : %s", p.getIfUnique());
|
||||||
|
t.line("ObjectProvider.getObject() : %s", Ctx.attempt(p::getObject));
|
||||||
|
var rf = Ctx.plain(OptionalConsumers.WithRequiredFalse.class);
|
||||||
|
t.line("@Autowired(required=false) setter: value=%s", rf.getBean(OptionalConsumers.WithRequiredFalse.class).notifier);
|
||||||
|
assertThat(required.started()).isFalse();
|
||||||
|
assertThat(opt.getBean(OptionalConsumers.WithOptional.class).notifier).isEmpty();
|
||||||
|
opt.close(); nul.close(); prov.close(); rf.close();
|
||||||
|
|
||||||
|
t.section("two Notifier beans (Sms, Email), no @Primary");
|
||||||
|
var two = Ctx.plain(EmailNotifier.class, SmsNotifier.class, OptionalConsumers.WithProvider.class);
|
||||||
|
var p2 = two.getBean(OptionalConsumers.WithProvider.class).provider;
|
||||||
|
t.line("ObjectProvider.getIfAvailable() : %s", Ctx.attempt(p2::getIfAvailable));
|
||||||
|
t.line("ObjectProvider.getIfUnique() : %s", p2.getIfUnique());
|
||||||
|
t.line("ObjectProvider.stream() classes : %s", p2.stream().map(n -> n.getClass().getSimpleName()).toList());
|
||||||
|
t.line("ObjectProvider.orderedStream() : %s", p2.orderedStream().map(n -> n.getClass().getSimpleName()).toList());
|
||||||
|
two.close();
|
||||||
|
var optTwo = Ctx.tryStart(EmailNotifier.class, SmsNotifier.class, OptionalConsumers.WithOptional.class);
|
||||||
|
assertThat(optTwo.started()).isFalse();
|
||||||
|
t.line("Optional<Notifier> with two beans: FAILED %s", Ctx.root(optTwo.failure()).getClass().getSimpleName());
|
||||||
|
t.line(" %s", Ctx.root(optTwo.failure()).getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collectionInjection() {
|
||||||
|
try (var t = new Transcript("14-list-map-set-injection.txt",
|
||||||
|
"List, Map and Set injection; registered Push, Email, Sms in that order")) {
|
||||||
|
try (var ctx = Ctx.plain(PushNotifier.class, EmailNotifier.class, SmsNotifier.class, Broadcaster.class)) {
|
||||||
|
var b = ctx.getBean(Broadcaster.class);
|
||||||
|
t.line("List<Notifier> order : %s", b.asList.stream().map(n -> n.getClass().getSimpleName()).toList());
|
||||||
|
t.line("Map<String,Notifier> : %s", b.asMap.keySet());
|
||||||
|
t.line("Set<Notifier> size : %d", b.asSet.size());
|
||||||
|
assertThat(b.asList.stream().map(n -> n.getClass().getSimpleName()).toList())
|
||||||
|
.containsExactly("SmsNotifier", "EmailNotifier", "PushNotifier");
|
||||||
|
assertThat(b.asMap.keySet()).containsExactly("pushNotifier", "emailNotifier", "smsNotifier");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyCollections() {
|
||||||
|
try (var t = new Transcript("15-empty-collection.txt", "A List<Plugin> injection point when zero Plugin beans exist")) {
|
||||||
|
t.section("required List<Plugin>, zero Plugin beans");
|
||||||
|
var plain = Ctx.tryStart(PluginHosts.Plain.class);
|
||||||
|
t.line("single constructor param : %s", plain.started() ? "started (empty list injected)" : "FAILED");
|
||||||
|
assertThat(plain.started()).isTrue();
|
||||||
|
assertThat(plain.context().getBean(PluginHosts.Plain.class)).isNotNull();
|
||||||
|
plain.closeQuietly();
|
||||||
|
var annotated = Ctx.tryStart(PluginHosts.TwoConstructors.class);
|
||||||
|
t.line("@Autowired ctor + no-arg ctor : %s", annotated.started() ? "started (empty list injected)" : "FAILED");
|
||||||
|
assertThat(annotated.started()).isTrue();
|
||||||
|
annotated.closeQuietly();
|
||||||
|
for (Class<?> host : new Class<?>[] {PluginHosts.FieldInjected.class, PluginHosts.SetterInjected.class}) {
|
||||||
|
var o = Ctx.tryStart(host);
|
||||||
|
assertThat(o.started()).isFalse();
|
||||||
|
t.line("%-26s : FAILED %s", host.getSimpleName(), Ctx.root(o.failure()).getClass().getSimpleName());
|
||||||
|
t.line(" %s", Ctx.root(o.failure()).getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
t.section("ways to make an absent collection legal");
|
||||||
|
var opt = Ctx.plain(PluginHosts.WithOptional.class);
|
||||||
|
t.line("Optional<List<Plugin>> : isPresent=%s", opt.getBean(PluginHosts.WithOptional.class).plugins.isPresent());
|
||||||
|
var nul = Ctx.plain(PluginHosts.WithNullable.class);
|
||||||
|
t.line("@Nullable List<Plugin> : value=%s", nul.getBean(PluginHosts.WithNullable.class).plugins);
|
||||||
|
var prov = Ctx.plain(PluginHosts.WithProvider.class);
|
||||||
|
t.line("ObjectProvider<Plugin> -> list : %s", prov.getBean(PluginHosts.WithProvider.class).plugins);
|
||||||
|
var rf = Ctx.plain(PluginHosts.WithRequiredFalse.class);
|
||||||
|
t.line("@Autowired(required=false) list: %s", rf.getBean(PluginHosts.WithRequiredFalse.class).plugins);
|
||||||
|
opt.close(); nul.close(); prov.close(); rf.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void genericsAndResource() {
|
||||||
|
try (var t = new Transcript("16-generics-and-resource.txt", "Generic type arguments and @Resource")) {
|
||||||
|
try (var ctx = Ctx.plain(StringHandler.class, IntegerHandler.class, GenericConsumer.class)) {
|
||||||
|
var c = ctx.getBean(GenericConsumer.class);
|
||||||
|
t.line("List<Handler<String>> : %s", c.stringHandlers.stream().map(h -> h.getClass().getSimpleName()).toList());
|
||||||
|
t.line("Handler<Integer> : %s", c.integerHandler.getClass().getSimpleName());
|
||||||
|
assertThat(c.stringHandlers).hasSize(1);
|
||||||
|
}
|
||||||
|
try (var ctx = Ctx.plain(StripeGateway.class, PaypalGateway.class, ResourceConsumer.class)) {
|
||||||
|
t.line("@Resource : %s", ctx.getBean(ResourceConsumer.class).describe());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dependencyGraph() {
|
||||||
|
try (var t = new Transcript("17-dependency-graph.txt",
|
||||||
|
"getDependenciesForBean: who did Spring wire into whom?")) {
|
||||||
|
try (var ctx = Ctx.plain(BaseBeans.class, ConstructorOrderService.class, SetterOrderService.class,
|
||||||
|
FieldOrderService.class)) {
|
||||||
|
var bf = ctx.getBeanFactory();
|
||||||
|
for (String name : new String[] {"constructorOrderService", "setterOrderService", "fieldOrderService"}) {
|
||||||
|
t.line("%-24s depends on %s", name, java.util.Arrays.toString(bf.getDependenciesForBean(name)));
|
||||||
|
}
|
||||||
|
t.line("%-24s is used by %s", "paymentGateway", java.util.Arrays.toString(bf.getDependentBeans("paymentGateway")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/** Post 16: what Spring does with A -> B -> A, in plain Spring and under Boot. */
|
||||||
|
class CircularDependencyTest {
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class ConstructorCycleConfig {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void plainSpringVersusBoot() {
|
||||||
|
try (var t = new Transcript("08-circular-plain-vs-boot.txt",
|
||||||
|
"Constructor cycle vs field cycle: plain Spring and Spring Boot")) {
|
||||||
|
|
||||||
|
t.section("plain Spring, constructor cycle (CtorA <-> CtorB)");
|
||||||
|
var ctor = Ctx.tryStart(CtorA.class, CtorB.class);
|
||||||
|
assertThat(ctor.started()).isFalse();
|
||||||
|
t.line("started: false");
|
||||||
|
t.line("root cause: %s", Ctx.root(ctor.failure()).getClass().getName());
|
||||||
|
t.line("%s", Ctx.root(ctor.failure()).getMessage());
|
||||||
|
|
||||||
|
t.section("plain Spring, field cycle (FieldA <-> FieldB)");
|
||||||
|
var field = Ctx.tryStart(FieldA.class, FieldB.class);
|
||||||
|
t.line("started: %s", field.started());
|
||||||
|
assertThat(field.started()).isTrue();
|
||||||
|
t.line("FieldA.b is FieldB: %s", field.context().getBean(FieldA.class).b == field.context().getBean(FieldB.class));
|
||||||
|
field.closeQuietly();
|
||||||
|
|
||||||
|
t.section("Spring Boot, field cycle, default settings");
|
||||||
|
String defaultBoot = bootStart(false, FieldA.class, FieldB.class);
|
||||||
|
t.line("%s", defaultBoot);
|
||||||
|
assertThat(defaultBoot).startsWith("FAILED");
|
||||||
|
|
||||||
|
t.section("Spring Boot, field cycle, spring.main.allow-circular-references=true");
|
||||||
|
String allowed = bootStart(true, FieldA.class, FieldB.class);
|
||||||
|
t.line("%s", allowed);
|
||||||
|
assertThat(allowed).startsWith("started");
|
||||||
|
|
||||||
|
t.section("Spring Boot, constructor cycle, spring.main.allow-circular-references=true");
|
||||||
|
String ctorAllowed = bootStart(true, CtorA.class, CtorB.class);
|
||||||
|
t.line("%s", ctorAllowed);
|
||||||
|
assertThat(ctorAllowed).startsWith("FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lazyBreaksAConstructorCycle() {
|
||||||
|
try (var t = new Transcript("09-lazy-breaks-constructor-cycle.txt",
|
||||||
|
"@Lazy on one constructor parameter breaks a constructor cycle")) {
|
||||||
|
var outcome = Ctx.tryStart(LazyA.class, LazyB.class);
|
||||||
|
assertThat(outcome.started()).isTrue();
|
||||||
|
var a = outcome.context().getBean(LazyA.class);
|
||||||
|
t.line("context started: true");
|
||||||
|
t.line("LazyA.b runtime class : %s", a.b.getClass().getName());
|
||||||
|
t.line("LazyA.b.hello() : %s", a.b.hello());
|
||||||
|
t.line("LazyA.b is the real LazyB bean: %s", a.b == outcome.context().getBean(LazyB.class));
|
||||||
|
assertThat(a.b.getClass().getName()).contains("SpringCGLIB");
|
||||||
|
outcome.closeQuietly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bootStart(boolean allowCircular, Class<?>... beans) {
|
||||||
|
var builder = new SpringApplicationBuilder(beans)
|
||||||
|
.web(WebApplicationType.NONE)
|
||||||
|
.logStartupInfo(false)
|
||||||
|
.properties("logging.level.root=OFF", "spring.main.banner-mode=off");
|
||||||
|
if (allowCircular) {
|
||||||
|
builder.properties("spring.main.allow-circular-references=true");
|
||||||
|
}
|
||||||
|
try (ConfigurableApplicationContext ctx = builder.run()) {
|
||||||
|
return "started";
|
||||||
|
} catch (Exception e) {
|
||||||
|
Throwable root = Ctx.root(e);
|
||||||
|
return "FAILED: " + root.getClass().getSimpleName() + ": " + root.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
|
||||||
|
/** Small helpers so each test reads as the scenario, not the plumbing. */
|
||||||
|
public final class Ctx {
|
||||||
|
|
||||||
|
private Ctx() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A plain Spring context (no Boot) with the given classes registered and refreshed. */
|
||||||
|
public static AnnotationConfigApplicationContext plain(Class<?>... classes) {
|
||||||
|
var ctx = new AnnotationConfigApplicationContext();
|
||||||
|
ctx.register(classes);
|
||||||
|
ctx.refresh();
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refreshes a context and returns either "started" or the exception, so failures can be printed. */
|
||||||
|
public static Outcome tryStart(Class<?>... classes) {
|
||||||
|
var ctx = new AnnotationConfigApplicationContext();
|
||||||
|
try {
|
||||||
|
ctx.register(classes);
|
||||||
|
ctx.refresh();
|
||||||
|
return new Outcome(ctx, null);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
ctx.close();
|
||||||
|
return new Outcome(null, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> String attempt(Callable<T> call) {
|
||||||
|
try {
|
||||||
|
return "OK -> " + call.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return e.getClass().getSimpleName() + ": " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Throwable root(Throwable t) {
|
||||||
|
while (t.getCause() != null && t.getCause() != t) {
|
||||||
|
t = t.getCause();
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Outcome(AnnotationConfigApplicationContext context, RuntimeException failure) {
|
||||||
|
|
||||||
|
public boolean started() {
|
||||||
|
return failure == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeQuietly() {
|
||||||
|
if (context != null) {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.ankurm.coredi.injection.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
/** Post 16: the same service, three injection styles. */
|
||||||
|
class InjectionStylesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void threeStylesUnderPlainNew() {
|
||||||
|
try (var t = new Transcript("01-three-styles-under-plain-new.txt",
|
||||||
|
"The same OrderService built with plain 'new', no Spring anywhere")) {
|
||||||
|
|
||||||
|
t.section("constructor injection");
|
||||||
|
var gateway = new LoggingPaymentGateway();
|
||||||
|
var notifier = new RecordingNotifier();
|
||||||
|
var ctor = new ConstructorOrderService(gateway, notifier);
|
||||||
|
t.line("new ConstructorOrderService(gateway, notifier).place(\"A-1\", 500)");
|
||||||
|
t.line(" %s", Ctx.attempt(() -> ctor.place("A-1", 500)));
|
||||||
|
t.line(" charges=%d, notifications=%d", gateway.charges().size(), notifier.sent().size());
|
||||||
|
|
||||||
|
t.section("setter injection, one setter forgotten");
|
||||||
|
var gateway2 = new LoggingPaymentGateway();
|
||||||
|
var setter = new SetterOrderService();
|
||||||
|
setter.setGateway(gateway2);
|
||||||
|
String setterResult = Ctx.attempt(() -> setter.place("A-2", 500));
|
||||||
|
t.line("new SetterOrderService(); setter.setGateway(gateway); // setNotifier never called");
|
||||||
|
t.line(" %s", setterResult);
|
||||||
|
t.line(" charges=%d <-- the customer was charged before the failure", gateway2.charges().size());
|
||||||
|
|
||||||
|
t.section("field injection");
|
||||||
|
var field = new FieldOrderService();
|
||||||
|
String fieldResult = Ctx.attempt(() -> field.place("A-3", 500));
|
||||||
|
t.line("new FieldOrderService().place(\"A-3\", 500)");
|
||||||
|
t.line(" %s", fieldResult);
|
||||||
|
var gateway3 = new LoggingPaymentGateway();
|
||||||
|
ReflectionTestUtils.setField(field, "gateway", gateway3);
|
||||||
|
ReflectionTestUtils.setField(field, "notifier", new RecordingNotifier());
|
||||||
|
t.line("after ReflectionTestUtils.setField(...) twice:");
|
||||||
|
t.line(" %s", Ctx.attempt(() -> field.place("A-3", 500)));
|
||||||
|
|
||||||
|
assertThat(setterResult).startsWith("NullPointerException");
|
||||||
|
assertThat(gateway2.charges()).hasSize(1);
|
||||||
|
assertThat(fieldResult).startsWith("NullPointerException");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldsAndFinality() {
|
||||||
|
try (var t = new Transcript("02-fields-and-finality.txt",
|
||||||
|
"Which injected fields can be final? (reflection over the three variants)")) {
|
||||||
|
for (Class<?> type : List.of(ConstructorOrderService.class, SetterOrderService.class,
|
||||||
|
FieldOrderService.class)) {
|
||||||
|
for (Field f : type.getDeclaredFields()) {
|
||||||
|
t.line("%-24s %-10s final=%s", type.getSimpleName(), f.getName(),
|
||||||
|
Modifier.isFinal(f.getModifiers()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Field f : ConstructorOrderService.class.getDeclaredFields()) {
|
||||||
|
assertThat(Modifier.isFinal(f.getModifiers())).isTrue();
|
||||||
|
}
|
||||||
|
for (Field f : FieldOrderService.class.getDeclaredFields()) {
|
||||||
|
assertThat(Modifier.isFinal(f.getModifiers())).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allThreeStylesSideBySide() {
|
||||||
|
Trace.drain();
|
||||||
|
try (var t = new Transcript("03-injection-order.txt",
|
||||||
|
"The order Spring touches one bean that uses all three styles")) {
|
||||||
|
try (var ctx = Ctx.plain(BaseBeans.class, AllThreeStyles.class)) {
|
||||||
|
Trace.drain().forEach(e -> t.line("%s", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldInjectedDependencyUsedInConstructor() {
|
||||||
|
try (var t = new Transcript("04-field-used-in-constructor.txt",
|
||||||
|
"A field-injected collaborator used in the constructor")) {
|
||||||
|
var outcome = Ctx.tryStart(BaseBeans.class, FieldTrapService.class);
|
||||||
|
t.line("context started: %s", outcome.started());
|
||||||
|
assertThat(outcome.started()).isFalse();
|
||||||
|
t.line("top exception : %s", outcome.failure().getClass().getName());
|
||||||
|
t.line("top message : %s", outcome.failure().getMessage());
|
||||||
|
t.line("root cause : %s", Ctx.root(outcome.failure()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multipleConstructors() {
|
||||||
|
Trace.drain();
|
||||||
|
try (var t = new Transcript("05-multiple-constructors.txt",
|
||||||
|
"Two constructors: which one does Spring pick?")) {
|
||||||
|
t.section("TwoConstructorsService: no-arg + one-arg, neither annotated");
|
||||||
|
try (var ctx = Ctx.plain(BaseBeans.class, TwoConstructorsService.class)) {
|
||||||
|
t.line("%s", Trace.drain());
|
||||||
|
t.line("gateway injected: %s", ctx.getBean(TwoConstructorsService.class).hasGateway());
|
||||||
|
assertThat(ctx.getBean(TwoConstructorsService.class).hasGateway()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
t.section("TwoConstructorsNoDefault: one-arg + two-arg, neither annotated");
|
||||||
|
var outcome = Ctx.tryStart(BaseBeans.class, TwoConstructorsNoDefault.class);
|
||||||
|
assertThat(outcome.started()).isFalse();
|
||||||
|
t.line("%s", outcome.failure().getClass().getName());
|
||||||
|
t.line("%s", outcome.failure().getMessage());
|
||||||
|
t.line("root cause: %s", Ctx.root(outcome.failure()));
|
||||||
|
|
||||||
|
t.section("TwoConstructorsAnnotated: @Autowired on one constructor");
|
||||||
|
try (var ctx = Ctx.plain(BaseBeans.class, TwoConstructorsAnnotated.class)) {
|
||||||
|
t.line("%s", Trace.drain());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optionalDependencyViaSetter() {
|
||||||
|
try (var t = new Transcript("06-optional-setter.txt", "@Autowired(required = false) on a setter")) {
|
||||||
|
try (var with = Ctx.plain(BaseBeans.class, OptionalAudit.class)) {
|
||||||
|
t.line("with a Notifier bean : %s", with.getBean(OptionalAudit.class).status());
|
||||||
|
}
|
||||||
|
try (var without = Ctx.plain(OptionalAudit.class)) {
|
||||||
|
t.line("without a Notifier bean : %s", without.getBean(OptionalAudit.class).status());
|
||||||
|
assertThat(without.getBean(OptionalAudit.class).status()).contains("no notifier");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void springWiresAllThreeIdentically() {
|
||||||
|
try (var t = new Transcript("07-spring-wires-all-three.txt", "Inside a Spring context all three styles work")) {
|
||||||
|
try (var ctx = Ctx.plain(BaseBeans.class, ConstructorOrderService.class, SetterOrderService.class,
|
||||||
|
FieldOrderService.class)) {
|
||||||
|
for (Class<? extends OrderService> type : List.of(ConstructorOrderService.class,
|
||||||
|
SetterOrderService.class, FieldOrderService.class)) {
|
||||||
|
t.line("%-24s -> %s", type.getSimpleName(), ctx.getBean(type).place("B-1", 700));
|
||||||
|
}
|
||||||
|
assertThat(ctx.getBean(PaymentGateway.class).charges()).hasSize(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ankurm.coredi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a numbered transcript under {@code output/} and echoes it to the console.
|
||||||
|
* Every console block quoted in the article comes out of one of these files verbatim.
|
||||||
|
*/
|
||||||
|
public final class Transcript implements AutoCloseable {
|
||||||
|
|
||||||
|
private final Path path;
|
||||||
|
private final StringWriter buffer = new StringWriter();
|
||||||
|
private final PrintWriter out = new PrintWriter(buffer);
|
||||||
|
|
||||||
|
public Transcript(String fileName, String title) {
|
||||||
|
this.path = Path.of("output", fileName);
|
||||||
|
out.println("# " + title);
|
||||||
|
out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript line(String format, Object... args) {
|
||||||
|
out.println(args.length == 0 ? format : String.format(format, args));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript blank() {
|
||||||
|
out.println();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript section(String heading) {
|
||||||
|
out.println();
|
||||||
|
out.println("--- " + heading + " ---");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
out.flush();
|
||||||
|
try {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, buffer.toString());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("could not write " + path, e);
|
||||||
|
}
|
||||||
|
System.out.print(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user