Add core-beans: bean scopes, the prototype-in-singleton trap, lifecycle callback order and graceful shutdown phases on Boot 4.1

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JoVmf2fWvcpoXndcDSwRf7
This commit is contained in:
Claude
2026-09-24 04:59:41 +00:00
parent 9825efa081
commit 51f3ecd5da
65 changed files with 1780 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# How many instances did the container really construct?
right after refresh():
singleton instances : 1
@Lazy singleton : 0
prototype : 0
after getBean() twice for singleton and lazy, three times for prototype:
singleton instances : 1 same object both times: true
@Lazy singleton : 1 same object both times: true
prototype : 3 serials: 1, 2, 3
two separate contexts, each with SingletonBean:
singleton instances : 2 same object across contexts: false
@@ -0,0 +1,4 @@
# @PreDestroy on a singleton and on a prototype, then context.close()
prototype instances created: 1
callbacks after close(): [singleton @PreDestroy called]
@@ -0,0 +1,11 @@
# A singleton calls use() five times. How many prototype instances did it touch?
how the prototype is obtained serials seen instances built
---------------------------------- ---------------------- ---------------
constructor injection (naive) [1, 1, 1, 1, 1] 1
ObjectProvider.getObject() [1, 2, 3, 4, 5] 5
@Lookup method [1, 2, 3, 4, 5] 5
scoped proxy (TARGET_CLASS) [1, 2, 3, 4, 5] 5
ApplicationContext.getBean() [1, 2, 3, 4, 5] 5
ProxyConsumer's injected reference is a com.ankurm.corebeans.scopes.ScopedPrototypeBean$$SpringCGLIB$$0
@@ -0,0 +1,4 @@
# Two threads call the same singleton; a latch forces the interleaving
UnsafeGreeter (state in a field) : alice's call returned "Hello, bob"
SafeGreeter (state in a local) : alice's call returned "Hello, alice"
+10
View File
@@ -0,0 +1,10 @@
# request, session and application scope over real HTTP
client response
A #1 request=1 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
A #2 request=2 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
A #3 request=3 session=1 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
B #1 request=4 session=2 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
B #2 request=5 session=2 application=1 injectedRequestClass=RequestBean$$SpringCGLIB$$0
instances constructed: request=5 session=2 application=1
@@ -0,0 +1,6 @@
# A request-scoped bean (no proxy) injected into a singleton
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
top message : Error creating bean with name 'brokenRequestConsumer': Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'brokenRequestConsumer.RawRequestBean': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton
root cause : java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
@@ -0,0 +1,26 @@
# Every callback for one bean, from constructor to the last destroy hook (SpringApplication, no web server)
=== startup ===
constructor
setter injection (@Autowired setDependency)
BeanNameAware.setBeanName("kitchenSink")
BeanFactoryAware.setBeanFactory
ApplicationContextAware.setApplicationContext
BeanPostProcessor.postProcessBeforeInitialization
@PostConstruct
InitializingBean.afterPropertiesSet
@Bean(initMethod = "customInit")
BeanPostProcessor.postProcessAfterInitialization
SmartInitializingSingleton.afterSingletonsInstantiated
SmartLifecycle.start (phase 2147483647)
ContextRefreshedEvent
ApplicationStartedEvent
ApplicationRunner.run
ApplicationReadyEvent
=== ctx.close() ===
ContextClosedEvent
SmartLifecycle.stop
@PreDestroy
DisposableBean.destroy
@Bean(destroyMethod = "customDestroy")
@@ -0,0 +1,3 @@
# A BeanPostProcessor declared with a non-static @Bean method
2026-09-24T10:28:59.979+05:30 WARN 3358 --- [core-beans] [ main] trationDelegate$BeanPostProcessorChecker : Bean 'nonStaticProcessorConfig' of type [com.ankurm.corebeans.lifecycle.NonStaticProcessorConfig$$SpringCGLIB$$0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). The currently created BeanPostProcessor [quietProcessor] is declared through a non-static factory method on that class; consider declaring it as static instead.
@@ -0,0 +1,4 @@
# @PostConstruct runs before the @Async proxy exists
@PostConstruct sees this = com.ankurm.corebeans.lifecycle.AsyncMailer, isAopProxy(this) = false
the bean other code receives : com.ankurm.corebeans.lifecycle.AsyncMailer$$SpringCGLIB$$0
@@ -0,0 +1,6 @@
# An exception thrown from @PostConstruct
context started: false
top exception : org.springframework.beans.factory.BeanCreationException
top message : Error creating bean with name 'failingInit': Invocation of init method failed
root cause : java.lang.IllegalStateException: cache warm-up failed
@@ -0,0 +1,10 @@
# Three SmartLifecycle beans registered in the order 300, 100, 200
=== refresh() ===
start A (phase 100)
start B (phase 200)
start C (phase 300)
=== close() ===
stop C (phase 300)
stop B (phase 200)
stop A (phase 100)
@@ -0,0 +1,4 @@
# Three SmartLifecycle beans in the SAME phase, each needing 400 ms to stop
stop(Runnable) blocks (the default) : close() took >= 1100 ms (sequential)
stop(Runnable) returns immediately : close() took < 800 ms (concurrent)
+4
View File
@@ -0,0 +1,4 @@
# Lifecycle vs SmartLifecycle: who starts at refresh()?
after refresh() : events=[] isRunning=false
after start() : events=[PlainLifecycle.start] isRunning=true
@@ -0,0 +1,4 @@
# A SmartLifecycle whose stop(callback) never calls the callback, timeout 500 ms
close() returned after roughly the timeout: true
2026-09-24T10:28:58.263+05:30 INFO 3358 --- [core-beans] [ main] o.s.c.support.DefaultLifecycleProcessor : Shutdown phase 2147483647 ends with 1 bean still running after timeout of 500ms: [neverStops]
@@ -0,0 +1,11 @@
# Every SmartLifecycle bean in a Boot web application, highest phase (stops first) at the top
phase bean name class
2147483647 earlyWorker EarlyWorker
2147482623 webServerGracefulShutdown WebServerGracefulShutdownLifecycle
2147481599 webServerStartStop WebServerStartStopLifecycle
1073741823 applicationTaskExecutor ThreadPoolTaskExecutor
1000 lateWorker LateWorker
-2147483647 springBootLoggingLifecycle Lifecycle
SmartLifecycle.DEFAULT_PHASE = 2147483647
@@ -0,0 +1,7 @@
# A /slow?ms=1500 request is in flight when the context closes
settings the client saw close() blocked handler ran to the end
-------------------------------------------- ---------------------------------------------------- -------------------------------- ----------------------
server.shutdown=graceful, virtual threads HTTP 200: finished after 1500 ms, virtual thread = true >= 1 s (waited for the request) yes
server.shutdown=graceful, platform threads HTTP 200: finished after 1500 ms, virtual thread = false >= 1 s (waited for the request) yes
server.shutdown=immediate, virtual threads FAILED: connection dropped, no response >= 1 s (waited for the request) yes
@@ -0,0 +1,5 @@
# SmartLifecycle beans with the default phase and with phase 1000, while a request is in flight
EarlyWorker.stop (phase 2147483647): requests still in flight = 1
LateWorker.stop (phase 1000): requests still in flight = 0
the client saw: HTTP 200: finished after 1500 ms, virtual thread = true
@@ -0,0 +1,5 @@
# Property defaults read from spring-configuration-metadata.json (Boot 4.1.1 jars)
spring.lifecycle.timeout-per-shutdown-phase type=java.time.Duration default=30s
spring.threads.virtual.enabled type=java.lang.Boolean default=False
server.shutdown type=org.springframework.boot.web.server.Shutdown default=graceful
@@ -0,0 +1,9 @@
# The BeanPostProcessors registered in a plain Spring Boot context, in the order they run
1 org.springframework.context.support.ApplicationContextAwareProcessor
2 org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor
3 org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker
4 com.ankurm.corebeans.lifecycle.TracingPostProcessor
5 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
6 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
7 org.springframework.context.support.ApplicationListenerDetector