Spring Framework 6.x Internals: Core Processes, Source Code & Learning Roadmap

This comprehensive guide covers Spring Framework 6.x and Spring Boot 3.x internals, including version migration, IoC/DI mechanisms, AOP proxy creation, bean lifecycle, circular dependency resolution, Spring MVC request processing, transaction management, auto-configuration, AOT compilation, and a structured learning roadmap with best practices.

Architecture Pony
Architecture Pony
Architecture Pony
Spring Framework 6.x Internals: Core Processes, Source Code & Learning Roadmap

Introduction

This document provides a technical analysis of Spring Framework 6.x / Spring Boot 3.x (Java 17+, Jakarta EE 9+), contrasting with Spring 5.x / Spring Boot 2.x (Java 8+, javax.*). It serves as an engineering reference manual combining technical analysis, core processes, core source code, and a learning roadmap.

Chapter 0: Document Conventions and Version Matrix

0.1 Reading Conventions

Symbols used throughout: ⭐ must-know for interviews and production; 🔑 key source code paths; ⚠️ error-prone areas; 🧩 extension points (SPI). Source code references use class and method names (e.g., AbstractApplicationContext#refresh()) as anchors.

0.2 Version Matrix

Key changes from Spring 5.x to 6.x:

JDK Baseline: Java 8 → Java 17 (breaking)

EE Namespace: javax.* → jakarta.* (breaking)

Embedded Container: Tomcat 9 → Tomcat 10 (breaking)

AOT / Native: None → GraalVM Native Image first-class support (new capability)

Declarative HTTP: RestTemplate → HTTP Interface (@HttpExchange) (new capability)

Web Stack: WebMVC / WebFlux → same + more complete reactive ecosystem (enhancement)

Observability: Micrometer 1.x → Micrometer Observation / Tracing (enhancement)

Auto-configuration Registration: spring.factories → AutoConfiguration.imports (since 2.7) (migration)

⚠️ Three commonly overlooked items when upgrading from Spring Boot 2.x to 3.x: ① all javax.* imports must change to jakarta.*; ② some third-party libraries (old MyBatis plugins, old Swagger) lack Jakarta versions; ③ spring.factories auto-configuration registration is deprecated.

Chapter 1: Spring Framework Overview

1.1 What is Spring

Spring is a lightweight, one-stop enterprise Java application framework with IoC and AOP at its core. Its essence is not writing business code but managing object creation and assembly while providing cross-cutting capabilities on invocation chains. In one sentence: Spring handles "how objects come into being, how they are wired, and how they are enhanced"; business code only handles "what objects do".

1.2 Evolution History

Four paradigm shifts:

XML → Annotations: Configuration moved from external files into code; @Configuration + @Bean became mainstream.

Library → Platform: Spring Boot turned "framework integration" into "out-of-the-box"; auto-configuration replaced massive manual XML.

Blocking → Reactive: WebFlux provides backpressure and async non-blocking for high-concurrency I/O-intensive scenarios.

Reflection → AOT: GraalVM native images require compile-time bean definition determination; Spring 6 introduced AOT processing phase.

1.3 Spring Ecosystem Landscape

Spring is a platform, not a single framework. Layered understanding:

Core Layer: Spring Framework – object management and cross-cutting enhancement.

Application Layer: Spring Boot – quickly build a usable monolithic application.

Distributed Layer: Spring Cloud / Alibaba – consistency, availability, observability after service decomposition.

Capability Layer: Security / Data / Batch – standardized integration of domain capabilities.

1.4 Technical Value

Decoupling: Callers depend on interfaces, not implementation construction details.

Testability: @SpringBootTest / pure unit tests + @MockBean make dependencies replaceable.

Cross-cutting Reuse: Transactions, logging, rate limiting, idempotency woven via AOP with zero business code intrusion.

Ecosystem Convergence: Different middleware (Kafka, RocketMQ, RabbitMQ) integrate via unified abstractions like MessageConverter, PlatformTransactionManager.

Observability: Micrometer Observation unifies metrics, logs, and tracing.

Chapter 2: Technical Analysis

2.1 Core Design Philosophy

Key principles and their Spring manifestations:

IoC (Inversion of Control): Object lifecycle managed by container; business code declares "what I need".

AOP (Aspect-Oriented Programming): Cross-cutting concerns (logging, transactions, permissions) separated from business logic.

Convention over Configuration: Spring Boot provides sensible defaults, override only when needed.

Template Method: JdbcTemplate, TransactionTemplate, RestTemplate unify resource management.

Strategy: PlatformTransactionManager, HandlerMapping, ViewResolver are replaceable.

Chain of Responsibility: HandlerInterceptor, Filter, MethodInterceptor chain calls.

Observer: ApplicationEvent / ApplicationListener event-driven.

Adapter: HandlerAdapter shields differences in Controller implementations.

2.2 Modular Architecture

Spring Framework Maven coordinates split into modules; on-demand inclusion is the source of its lightweight nature. Key modules: spring-core: Resource abstraction, @Nullable/@NonNull contracts, low-level utilities (ClassUtils, ReflectionUtils). spring-beans: BeanFactory, BeanDefinition, BeanPostProcessor, PropertyEditor. spring-context: ApplicationContext, @Component scanning, events, i18n, Environment abstraction. spring-expression: SpEL expression engine, foundation for @Value("#{...}"). spring-aop: Proxy-based AOP implementation (ProxyFactory, Advisor). spring-tx: Declarative/programmatic transaction abstraction. spring-webmvc: DispatcherServlet, HandlerMapping, ViewResolver.

2.3 IoC / DI Technical Essence

IoC is the idea, DI is the implementation. IoC: control of object creation shifts from caller code to container. DI: container actively injects dependencies. Three injection modes:

Constructor Injection (e.g., public OrderService(PayClient c)): dependencies immutable, test-friendly, exposes circular dependencies – ⭐ preferred .

Setter Injection (e.g., @Autowired setPayClient(...)): flexible for optional dependencies – secondary.

Field Injection (e.g., @Autowired private PayClient c): concise but hard to unit test, hides dependencies, cannot be final – ⚠️ avoid (except tests) .

Why constructor injection is preferred: dependencies can be final (immutable, thread-safe); unit tests can directly new without Spring; circular dependencies fail fast at startup rather than causing NPE at runtime; Spring officially recommends constructor injection for mandatory dependencies.

⚠️ Constructor injection circular dependencies cannot be solved via three-level cache; Spring throws BeanCurrentlyInCreationException. This is a feature, not a bug – it forces design review.

2.4 AOP Technical Essence

Core problem: insert logic before/after method calls without modifying source. Three implementation paths:

Static Weaving: AspectJ compiler modifies bytecode at compile time; requires spring-aspects + LTW.

Dynamic Proxy (JDK): Implements interfaces via InvocationHandler; runtime; ⭐ default .

Dynamic Proxy (CGLIB): Subclasses target class, overrides methods; runtime; ⭐ default (Boot) .

AOP terminology mapped to Spring types:

Aspect: @Aspect-annotated class.

Join Point: Method execution point (Spring AOP supports only method-level).

Pointcut: Pointcut / AspectJExpressionPointcut.

Advice: Before / After / Around / AfterReturning / AfterThrowing.

Weaving: AbstractAutoProxyCreator.

Advisor: Pointcut + Advice combination; PointcutAdvisor.

🔑 Key insight: Spring AOP "aspects" are ultimately converted to Advisor collections, then ProxyFactory generates proxy objects. @Aspect is merely AspectJ-style syntactic sugar; runtime still uses Spring's own proxy system.

2.5 Horizontal Technical Comparisons

Spring MVC vs WebFlux

Programming Model: Blocking/imperative vs Non-blocking/reactive (Reactor).

Thread Model: One thread per request (Servlet container) vs Event loop (Netty), few threads.

Return Types: Plain objects / ResponseEntity vs Mono / Flux.

Suitable Scenarios: Traditional CRUD, CPU-intensive vs High-concurrency I/O-intensive, gateway, streaming.

Blocking Library Interop: Native vs ⚠️ Blocking calls stall event loop.

Learning Cost: Low vs High (requires Reactive Streams mastery).

Spring Boot vs Native Spring

Configuration: Manual XML / @Configuration vs Auto-configuration + application.yml.

Dependency Management: Declare versions individually vs Starter dependencies unify versions.

Deployment: External WAR + container vs Embedded container, java -jar.

Operations: Self-integrate monitoring vs Actuator out-of-the-box.

Flexibility: Full control vs Must understand auto-configuration touchpoints to override precisely.

2.6 Key Metrics and Selection Advice

Startup Time: Spring Boot 3 + AOT significantly compresses cold start; with Native Image moves from seconds to hundreds of milliseconds.

Proxy Choice: Spring Boot 2.x+ defaults to spring.aop.proxy-target-class=true (unconditional CGLIB) to avoid "must implement interface" constraint.

Circular Dependencies: Spring Boot 2.6+ defaults spring.main.allow-circular-references=false; must explicitly enable.

Virtual Threads: Spring Boot 3.2+ provides spring.threads.virtual.enabled=true to handle requests with virtual threads on Servlet stack.

Chapter 3: Core Process Analysis

3.1 IoC Container Startup Overall Flow

Everything starts from AbstractApplicationContext#refresh(), the sole main path of Spring startup, 12 steps:

prepareRefresh() – record start time, switch active flag, init placeholder property sources, validate required properties.

obtainFreshBeanFactory() – get/create BeanFactory, load BeanDefinitions (XML parsing, annotation scanning here).

prepareBeanFactory(beanFactory) – set ClassLoader, SpEL resolver, register ApplicationContextAwareProcessor, etc.

postProcessBeanFactory(beanFactory) – subclass extension hook (e.g., web container registers request/session scopes).

invokeBeanFactoryPostProcessors(beanFactory) – ⭐ execute BeanFactoryPostProcessors (ConfigurationClassPostProcessor completes component scanning and @Bean parsing here).

registerBeanPostProcessors(beanFactory) – register BeanPostProcessors (AOP proxy creator enters pipeline).

initMessageSource() – initialize i18n message source.

initApplicationEventMulticaster() – initialize event broadcaster.

onRefresh() – subclass extension hook (Spring MVC initializes DispatcherServlet here).

registerListeners() – register listeners, publish early events.

finishBeanFactoryInitialization(beanFactory) – ⭐ instantiate all non-lazy singletons (entry to bean lifecycle).

finishRefresh() – clear caches, start LifecycleProcessor, publish ContextRefreshedEvent.

Mnemonic: "Prepare new factory, post-process, register post-processors, broadcast messages, instantiate all singletons, finish refresh."

Three confusing points:

BeanFactoryPostProcessor vs BeanPostProcessor: Former acts on BeanDefinition (blueprint), latter on Bean instance (product).

invokeBeanFactoryPostProcessors vs registerBeanPostProcessors order: Modify blueprint first, then install assembly line, finally produce product.

onRefresh vs finishRefresh: Former is "during refresh" (subclass initializes internal components), latter is "after refresh" (publish events).

3.2 Bean Lifecycle Full Process ⭐

Most core, highest-frequency interview process. Key conclusions:

@PostConstruct executes before afterPropertiesSet because former driven by BeanPostProcessor's before hook.

AOP proxy generated in postProcessAfterInitialization , so injected into other beans is proxy object, not raw object.

Singleton bean destruction occurs at container close(); prototype bean destruction not managed by Spring.

3.3 Dependency Injection Flow

@Autowired resolution by AutowiredAnnotationBeanPostProcessor during property population phase (populateBean → InstantiationAwareBeanPostProcessor.postProcessProperties).

Comparison of @Autowired / @Resource / @Inject:

@Autowired (Spring): byType then byName (with @Qualifier).

@Resource (JSR-250): byName then byType.

@Inject (JSR-330): same as @Autowired, requires extra dependency.

3.4 Three-Level Cache and Circular Dependencies ⭐

Circular dependency: A depends on B, B depends on A, forming a loop. Spring uses "three-level cache" to solve singleton + setter/field injection circular dependencies:

Level 1: singletonObjects – fully initialized finished beans.

Level 2: earlySingletonObjects – instantiated but unpopulated half-finished beans (early references).

Level 3: singletonFactories – ObjectFactory for generating early references ( used for early AOP proxying ).

Why three levels instead of two? For AOP semantic correctness . If B needs proxied A, B must get correct proxy shape before A finishes initialization. Level 3's ObjectFactory provides "lazy decision": only when circular dependency actually occurs does it call getEarlyBeanReference to generate proxy; if no circular dependency, normal postProcessAfterInitialization path avoids duplicate proxying.

Three scenarios three-level cache cannot solve:

Constructor injection circular dependency – object cannot be instantiated, ObjectFactory never registered.

Prototype bean circular dependency – prototypes not cached, immediate exception.

@Async proxy circular dependency – AsyncAnnotationBeanPostProcessor is ordinary BeanPostProcessor, cannot expose early.

Spring Boot 2.6+ defaults to disabling circular dependency support; need spring.main.allow-circular-references=true to enable. Official stance: circular dependency is a design problem, not a configuration problem.

3.5 AOP Proxy Creation Flow

Proxy creation occurs in AnnotationAwareAspectJAutoProxyCreator's postProcessAfterInitialization. Interception chain at call time uses ReflectiveMethodInvocation.proceed() – classic Chain of Responsibility + recursion: each MethodInterceptor receives the invocation, decides when to call proceed() – @Around's before/after semantics naturally emerge.

⚠️ Most famous AOP pitfall: self-invocation fails. Reason: proxy interception only occurs on external calls via proxy object; this calls bypass proxy. Solutions: ① extract to another bean; ② inject self proxy (AopContext.currentProxy(), requires exposeProxy=true); ③ use TransactionTemplate for programmatic transactions.

3.6 Spring MVC Request Processing Flow ⭐

DispatcherServlet initializes nine components:

HandlerMapping – request → Handler mapping.

HandlerAdapter – unified Handler invocation.

HandlerExceptionResolver – exception → view/status code.

ViewResolver – logical view name → View.

LocaleResolver – i18n locale.

ThemeResolver – theme.

MultipartResolver – file upload.

FlashMapManager – redirect attributes.

RequestToViewNameTranslator – infer view name when absent.

Key extension points 🧩:

HandlerInterceptor: preHandle / postHandle / afterCompletion – auth, logging, idempotency.

RequestBodyAdvice / ResponseBodyAdvice: unified request/response body processing – encryption, desensitization.

@ControllerAdvice + @ExceptionHandler: global exception handling.

WebMvcConfigurer: unified registration for interceptors, message converters, CORS.

3.7 Declarative Transaction Execution Flow

⚠️ Seven causes of transaction failure (must-know for production):

Self-invocation: this.method() bypasses proxy.

Method not public: Spring 5+ relaxed but non-public risky in some proxies.

Exception swallowed: try-catch without rethrow – transaction framework unaware.

Exception type mismatch: Default rolls back only RuntimeException and Error; checked exceptions need rollbackFor = Exception.class.

Improper propagation: e.g., NOT_SUPPORTED, NEVER suspend or prohibit transactions.

Database engine unsupported: MySQL MyISAM doesn't support transactions.

Multiple data sources without specified transaction manager: @Transactional(transactionManager = "xxxTxManager").

3.8 Spring Boot Auto-configuration Flow ⭐

@SpringBootApplication

three-in-one decomposition:

@SpringBootConfiguration  // essentially @Configuration, marks main config class
@EnableAutoConfiguration  // enables auto-configuration (core)
@ComponentScan            // scans main class package and sub-packages
public @interface SpringBootApplication { }

Conditional annotation system (auto-configuration switches):

@ConditionalOnClass – class present in classpath.

@ConditionalOnMissingBean – bean absent in container.

@ConditionalOnProperty – config property satisfies condition.

@ConditionalOnWebApplication – current is web application.

@ConditionalOnResource – resource file exists.

@ConditionalOnExpression – SpEL expression true.

🧩 Three ways to override auto-configuration: ① Config property override (spring.datasource.url=...); ② Custom @Bean override (leverages @ConditionalOnMissingBean); ③ Exclude auto-configuration ( @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) ).

3.9 Event Publishing Mechanism

Built-in events: ContextRefreshedEvent, ContextClosedEvent, ApplicationReadyEvent, ApplicationFailedEvent. @TransactionalEventListener: binds to transaction phases (BEFORE_COMMIT / AFTER_COMMIT / AFTER_ROLLBACK) – standard way to "send message/notification after transaction commit".

⚠️ Default synchronous events fire before transaction commit ; listeners querying DB may not see uncommitted data – this is why @TransactionalEventListener exists.

Chapter 4: Core Source Code Analysis

4.1 AbstractApplicationContext#refresh() 🔑

Full method with comments showing 12 steps. Key takeaways:

Entire refresh() protected by synchronized (startupShutdownMonitor) – same container cannot refresh concurrently .

Steps 5, 6, 11 are main battleground for annotation-driven Spring; others are environment prep and extension points. finally block calls resetCommonCaches() to clean reflection metadata caches, preventing classloader leaks (critical for hot deploy).

4.2 invokeBeanFactoryPostProcessors – Annotation-Driven Starting Point 🔑

PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors core logic:

First execute BeanDefinitionRegistryPostProcessors (can add BeanDefinitions); ConfigurationClassPostProcessor has highest priority (PriorityOrdered → Ordered → plain).

Then execute ordinary BeanFactoryPostProcessors (can only modify existing BeanDefinitions).

What ConfigurationClassPostProcessor does: processes @Configuration classes, @ComponentScan, @Import, @Bean methods.

Two key details:

@Configuration classes are CGLIB-enhanced (proxyBeanMethods = true default), so @Bean method inter-calls return same singleton, not new each time. This is essence of "Full mode" vs "Lite mode" (@Bean inside @Component).

@Import three usages: import plain class (register as bean), import ImportSelector (conditional batch import), import ImportBeanDefinitionRegistrar (manual BeanDefinition registration). Spring Boot auto-configuration is typical ImportSelector application.

4.3 registerBeanPostProcessors 🔑

Registration order determines subsequent execution priority:

PriorityOrdered BeanPostProcessors

Ordered BeanPostProcessors

Non-Ordered BeanPostProcessors

MergedBeanDefinitionPostProcessors (registered last, sorted)

Re-register ApplicationListenerDetector (ensures it executes last)

Execution order rules (critical):

postProcessBeforeInitialization – executes in registration order (forward).

postProcessAfterInitialization – executes in reverse registration order.

A bean's final shape determined by last postProcessAfterInitialization – hence AOP proxy creator must be positioned appropriately.

Common built-in BeanPostProcessors and responsibilities:

AutowiredAnnotationBeanPostProcessor – handles @Autowired / @Value / @Inject.

CommonAnnotationBeanPostProcessor – handles @PostConstruct / @PreDestroy / @Resource.

AnnotationAwareAspectJAutoProxyCreator – AOP proxy creation (carries @Aspect).

AsyncAnnotationBeanPostProcessor – handles @Async.

ApplicationContextAwareProcessor – callbacks EnvironmentAware / ApplicationContextAware etc.

ApplicationListenerDetector – registers ApplicationListener to event broadcaster.

4.4 finishBeanFactoryInitialization → preInstantiateSingletons 🔑

DefaultListableBeanFactory#preInstantiateSingletons simplified:

public void preInstantiateSingletons() {
    List beanNames = new ArrayList(this.beanDefinitionNames);
    for (String beanName : beanNames) {
        RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
            if (isFactoryBean(beanName)) {
                Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); // prefix "&"
                if (bean instanceof SmartFactoryBean<?> smartFactoryBean
                        && smartFactoryBean.isEagerInit()) {
                    getBean(beanName); // actually get product from FactoryBean
                }
            } else {
                getBean(beanName); // ⭐ ordinary bean enters lifecycle here
            }
        }
    }
    // After all singletons ready, callback SmartInitializingSingleton#afterSingletonsInstantiated
    for (String beanName : beanNames) {
        Object singletonInstance = getSingleton(beanName);
        if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
            smartSingleton.afterSingletonsInstantiated();
        }
    }
}

Key points:

Only non-abstract, singleton, non-lazy beans are eagerly instantiated.

getBean(beanName) is unified lifecycle entry, whether from DI or active retrieval.

SmartInitializingSingleton#afterSingletonsInstantiated is final callback after all singletons ready, suitable for global initialization.

4.5 doGetBean and Three-Level Cache Source 🔑

AbstractBeanFactory#doGetBean main flow:

Transform bean name (handle & prefix and aliases).

Try get finished bean from cache (three-level cache exit).

Prototype bean circular dependency: immediate fail (no cache, cannot solve).

Check parent factory.

Merge BeanDefinition, mark bean as currently creating (for circular detection).

Create dependencies first (@DependsOn and constructor/field dependencies recursively expanded).

Create by scope: singleton / prototype / others.

DefaultSingletonBeanRegistry#getSingleton (three-level cache core) 🔑:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // ⭐ here getObject() may generate AOP proxy early
                            singletonObject = singletonFactory.getObject();
                            // Promote: level 3 → level 2 (half-finished), delete level 3 to avoid duplicate generation
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

Three caches write timing:

doCreateBean after instantiation (after createBeanInstance) → addSingletonFactory → write level 3.

getSingleton(beanName, allowEarlyReference=true) hits level 3 → promote to level 2, delete level 3.

addSingleton after full initialization → write level 1, clean level 2 and 3.

4.6 doCreateBean – Bean Lifecycle Source Mainline 🔑

Four phases:

Instantiation: createBeanInstance (reflection constructor, resolves constructor dependencies).

Early exposure: addSingletonFactory to level 3 cache (key to solving circular dependencies).

Property Population: populateBean – @Autowired injected here via InstantiationAwareBeanPostProcessor#postProcessProperties.

Initialization: initializeBean – Aware callbacks, BeanPostProcessor.postProcessBeforeInitialization (@PostConstruct here), InitializingBean.afterPropertiesSet() / custom init-method, BeanPostProcessor.postProcessAfterInitialization (AOP proxy generated here, returned may be proxy).

Circular dependency reference validation: if early exposure occurred, check level 2 cache.

Register destruction callback (DisposableBeanAdapter).

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPIoCSource Code AnalysisAOT CompilationCircular DependencyAuto-configurationSpring FrameworkBean LifecycleLearning RoadmapSpring Boot 3.x
Architecture Pony
Written by

Architecture Pony

Learning notes

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.