π AOP & DI/IoC Integration
Overviewβ
This document provides an in-depth technical analysis of how Aspect-Oriented Programming (AOP) is integrated with the DI/IoC container in the Sprout Framework to perform automatic proxy creation. By examining the entire processβfrom the initialization order of infrastructure beans to CGLIB-based proxy generation and the method interception chainβthis analysis aims to provide a complete understanding of Sprout AOP's operational mechanism.
Overall Architecture Overviewβ
AOP-DI Integration Flowchartβ
Application Start
β
SproutApplicationContext.refresh()
β
1. Scan Bean Definitions (scanBeanDefinitions)
βββ Scan @Component, @Service, @Repository
βββ Scan @Aspect classes
βββ Classify into InfrastructureBean vs. ApplicationBean
β
2. Initialize Infrastructure Beans (instantiateInfrastructureBeans)
βββ Create AdvisorRegistry, AdviceFactory, ProxyFactory
βββ Create and register AspectPostProcessor
βββ Execute PostInfrastructureInitializer
β
3. Execute AopPostInfrastructureInitializer
βββ Scan @Aspect classes
βββ Create Advisors and register them in the registry
βββ Initialize AspectPostProcessor
β
4. Register BeanPostProcessors (registerBeanPostProcessors)
βββ Register AspectPostProcessor as a BeanPostProcessor
β
5. Initialize Application Beans (instantiateAllSingletons)
βββ Execute BeanPostProcessor chain during bean creation
βββ Invoke AspectPostProcessor.postProcessAfterInitialization
βββ Determine need for proxy and create CGLIB proxy
βββ Set up method interception with BeanMethodInterceptor
Core Design Principlesβ
- Infrastructure-First Initialization: AOP-related infrastructure beans are initialized before application beans.
- PostProcessor Pattern: Transparent proxy creation is achieved through the
BeanPostProcessor
pattern. - CGLIB-Based Proxies: Enables proxy creation even for classes that do not implement an interface.
- Chain of Responsibility: Ensures the sequential execution of multiple advices.
Infrastructure Bean Initialization Mechanismβ
1. SproutApplicationContext
's Initialization Strategyβ
Step-by-Step Initialization Process
@Override
public void refresh() throws Exception {
scanBeanDefinitions(); // 1. Scan bean definitions
instantiateInfrastructureBeans(); // 2. Initialize infrastructure beans (including AOP)
instantiateAllSingletons(); // 3. Initialize application beans
// 4. Context post-processing
List<ContextInitializer> contextInitializers = getAllBeans(ContextInitializer.class);
for (ContextInitializer initializer : contextInitializers) {
initializer.initializeAfterRefresh(this);
}
}
2. Bean Classification Strategy: Infrastructure vs. Applicationβ
Automatic Classification Algorithm
private void scanBeanDefinitions() throws NoSuchMethodException {
// Scan all bean definitions
Collection<BeanDefinition> allDefs = scanner.scan(configBuilder,
Component.class, Controller.class, Service.class, Repository.class,
Configuration.class, Aspect.class, ControllerAdvice.class, WebSocketHandler.class
);
// Classify infrastructure beans (BeanPostProcessor + InfrastructureBean)
List<BeanDefinition> infraDefs = new ArrayList<>(allDefs.stream()
.filter(bd -> BeanPostProcessor.class.isAssignableFrom(bd.getType()) ||
InfrastructureBean.class.isAssignableFrom(bd.getType()))
.toList());
// Classify application beans (the rest)
List<BeanDefinition> appDefs = new ArrayList<>(allDefs);
appDefs.removeAll(infraDefs);
this.infraDefs = infraDefs;
this.appDefs = appDefs;
}
Classification Criteria
- Infrastructure Beans: Implementations of
BeanPostProcessor
+InfrastructureBean
. - Application Beans: All remaining beans (business logic beans).
Importance of Classification
- Guaranteed Order: Ensures the AOP infrastructure is ready before application beans are created.
- Dependency Resolution: Makes
PostProcessor
s available at the time of application bean creation. - Initialization Separation: Allows for independent initialization strategies for each group.
3. PostInfrastructureInitializer
Patternβ
Callback After Infrastructure Bean Initialization
private void instantiateInfrastructureBeans() {
instantiateGroup(infraDefs); // Create infrastructure beans
// Execute PostInfrastructureInitializers
List<PostInfrastructureInitializer> initializers = beanFactory.getAllBeans(PostInfrastructureInitializer.class);
for (PostInfrastructureInitializer initializer : initializers) {
initializer.afterInfrastructureSetup(beanFactory, basePackages);
}
}
AopPostInfrastructureInitializer
Implementation
@Component
public class AopPostInfrastructureInitializer implements PostInfrastructureInitializer {
private final AspectPostProcessor aspectPostProcessor;
@Override
public void afterInfrastructureSetup(BeanFactory beanFactory, List<String> basePackages) {
aspectPostProcessor.initialize(basePackages); // Initialize the AspectPostProcessor
}
}
Importance of Initialization Timing
- Executes after all AOP-related infrastructure beans (
AdvisorRegistry
,AdviceFactory
, etc.) are ready. - Ensures all
Advisor
s are registered before application beans are created. - Completes AOP setup before
BeanPostProcessor
s are registered.
AspectPostProcessor: The Core Engine of AOPβ
1. Dual-Role Architectureβ
AspectPostProcessor
performs two critical roles at different stages:
- During
PostInfrastructureInitializer
: Scans for Aspects and registersAdvisor
s. - During
BeanPostProcessor
: Determines whether a proxy is needed and creates it.
2. Aspect Scanning and Advisor Registration Processβ
Initialization Method
public void initialize(List<String> basePackages) {
if (initialized.compareAndSet(false, true)) { // Use AtomicBoolean to prevent duplicate execution
this.basePackages = basePackages;
scanAndRegisterAdvisors();
}
}
Scanning Based on the Reflections
Library
private void scanAndRegisterAdvisors() {
// Set up scan scope with ConfigurationBuilder
ConfigurationBuilder configBuilder = new ConfigurationBuilder();
for (String pkg : basePackages) {
configBuilder.addUrls(ClasspathHelper.forPackage(pkg));
}
configBuilder.addScanners(Scanners.TypesAnnotated, Scanners.SubTypes);
// Filter by package
FilterBuilder filter = new FilterBuilder();
for (String pkg : basePackages) {
filter.includePackage(pkg);
}
configBuilder.filterInputsBy(filter);
// Find classes annotated with @Aspect
Reflections reflections = new Reflections(configBuilder);
Set<Class<?>> aspectClasses = reflections.getTypesAnnotatedWith(Aspect.class);
// Create and register Advisors from each Aspect class
for (Class<?> aspectClass : aspectClasses) {
List<Advisor> advisorsForThisAspect = createAdvisorsFromAspect(aspectClass);
for (Advisor advisor : advisorsForThisAspect) {
advisorRegistry.registerAdvisor(advisor);
}
}
}
Creating Advisors from an Aspect
private List<Advisor> createAdvisorsFromAspect(Class<?> aspectClass) {
List<Advisor> advisors = new ArrayList<>();
// Supplier for looking up the bean from the ApplicationContext
Supplier<Object> aspectSupplier = () -> container.getBean(aspectClass);
// Iterate over all methods to find advice annotations
for (Method m : aspectClass.getDeclaredMethods()) {
adviceFactory.createAdvisor(aspectClass, m, aspectSupplier)
.ifPresent(advisors::add);
}
return advisors;
}
3. Proxy Creation as a BeanPostProcessor
β
Post-Processing Method
@Override
public Object postProcessAfterInitialization(String beanName, Object bean) {
Class<?> targetClass = bean.getClass();
// Determine if a proxy is needed
boolean needsProxy = false;
for (Method method : targetClass.getMethods()) {
if (Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())) {
if (!advisorRegistry.getApplicableAdvisors(targetClass, method).isEmpty()) {
needsProxy = true;
break;
}
}
}
// Create and return the proxy
if (needsProxy) {
CtorMeta meta = container.lookupCtorMeta(bean);
return proxyFactory.createProxy(targetClass, bean, advisorRegistry, meta);
}
return bean; // Return the original bean if no proxy is needed
}
Optimization for Proxy Necessity Check
- Check Public Methods Only:
private
/protected
methods are not AOP targets. - Exclude Static Methods: Only instance methods can be intercepted.
- Early Exit: Create a proxy as soon as the first applicable
Advisor
is found. - Utilize Cache: Leverages the per-method caching in
AdvisorRegistry
.
CGLIB-Based Proxy Creation Systemβ
1. CglibProxyFactory
: The Proxy Creation Specialistβ
Concise Proxy Creation
@Component
public class CglibProxyFactory implements ProxyFactory, InfrastructureBean {
@Override
public Object createProxy(Class<?> targetClass, Object target, AdvisorRegistry registry, CtorMeta meta) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass); // Inheritance-based proxy
enhancer.setCallback(new BeanMethodInterceptor(target, registry)); // Set method interceptor
return enhancer.create(meta.paramTypes(), meta.args()); // Create instance with constructor parameters
}
}
CGLIB Enhancer
Configuration
setSuperclass
: Sets the original class as the superclass (for an inheritance-based proxy).setCallback
: Sets the callback that will intercept all method calls.create
: Creates the proxy instance using the same constructor parameters as the original object.
Leveraging CtorMeta
- Preserves the constructor information used to create the original bean.
- Ensures the proxy is created with the same constructor parameters.
- Guarantees constructor consistency within the DI container.
2. BeanMethodInterceptor
: The Method Interception Hubβ
CGLIB MethodInterceptor
Implementation
public class BeanMethodInterceptor implements MethodInterceptor {
private final Object target; // The original object
private final AdvisorRegistry advisorRegistry; // The advisor registry
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// Look up applicable advisors (uses cache)
List<Advisor> applicableAdvisors = advisorRegistry.getApplicableAdvisors(target.getClass(), method);
if (applicableAdvisors.isEmpty()) {
// If no advisors, invoke the original method directly
return proxy.invoke(target, args);
}
// Create a MethodInvocation to execute the advice chain
MethodInvocationImpl invocation = new MethodInvocationImpl(target, method, args, proxy, applicableAdvisors);
return invocation.proceed();
}
}
Interception Optimization Strategy
- Early Branching: Invokes the original method immediately if no advisors apply.
- Cache Utilization: Leverages
AdvisorRegistry
's per-method advisor caching. - Lazy Creation:
MethodInvocation
is created only when needed. - Direct Invocation: CGLIB's
MethodProxy.invoke()
provides optimized performance.
Proxy Strategies: Delegating vs. Subclassingβ
In Sprout AOP, proxy creation follows two main strategies.
1. Delegating Proxyβ
- Structure: The original instance is created first, and the proxy simply delegates calls to it
- Interceptor behavior:
proxy.invoke(target, args)
- Characteristics:
- Both the original object and the proxy exist
- The constructor of the original may run twice (original creation + proxy creation)
- Objenesis is used to skip proxy constructor execution, preventing the βdouble instantiationβ issue
- When to use: When the original objectβs state or constructor logic must be preserved
2. Subclassing Proxyβ
- Structure: CGLIB generates a subclass of the original class, and this subclass is the bean
- Interceptor behavior:
proxy.invokeSuper(this, args)
- Characteristics:
- No separate original instance exists
- The chosen constructor is called only once when creating the proxy, so the βdouble instantiationβ problem does not occur
- Dependency injection (DI) is applied directly to the proxy instance (constructor/field/setter all target the proxy)
- When to use: When the proxy itself should serve as the bean, and the original object does not need to be managed separately
Sproutβs Choiceβ
Sprout adopts the Subclassing Proxy strategy as the default.
This approach is structurally simple, removes the βconstructor called twiceβ problem, and integrates naturally with the DI container.
Specifically:
- Aspect classes are registered in the registry as regular beans with DI already completed
- Application beans are proxy instances, with constructor DI performed only once
- If circular dependencies arise, they are resolved by re-entering
getBean()
This allows developers to inject dependencies transparently, without worrying about whether a bean is proxied.
Objenesis Fallback: Supporting Delegating AOPβ
Sprout uses the Subclassing Proxy model by default. However, to support Delegating AOP in the future, an Objenesis-based fallback path is required.
Why Objenesis?β
- In the delegating model, the proxy already has a reference to the original instance
- If
enhancer.create(..)
is used directly:- The proxyβs creation process will trigger the superclass constructor again
- This results in the original constructor running twice (once for the original + once for the proxy)
- This can lead to unwanted side effects, reinitialization of final fields, or duplicated resource setup
- Therefore, a mechanism is needed to create a bean without calling its constructor β Objenesis
Example Fallback Pathβ
@Component
public class CglibProxyFactory implements ProxyFactory, InfrastructureBean {
@Override
public Object createProxy(Class<?> targetClass, Object target, AdvisorRegistry registry, CtorMeta meta) {
Enhancer e = new Enhancer();
e.setSuperclass(targetClass);
if (target != null) {
// Delegating Proxy path: target already exists β use Objenesis to skip ctor
e.setCallbackType(MethodInterceptor.class);
Class<?> proxyClass = e.createClass();
Object proxy = objenesis.newInstance(proxyClass); // Skip constructor
((Factory) proxy).setCallback(0, new BeanMethodInterceptor(target, registry));
return proxy;
} else {
// Subclassing Proxy path: proxy itself is the bean β normal constructor call
e.setCallback(new BeanMethodInterceptor(null, registry));
return e.create(meta.paramTypes(), meta.args());
}
}
}
MethodInvocation
Chain Execution Systemβ
1. MethodInvocationImpl
: Implementing the Chain of Responsibilityβ
Managing the Advice Chain State
public class MethodInvocationImpl implements MethodInvocation {
private final Object target; // The original object
private final Method method; // The method to be invoked
private final Object[] args; // Method arguments
private final MethodProxy methodProxy; // CGLIB method proxy
private final List<Advisor> advisors; // List of applicable advisors
private int currentAdvisorIndex = -1; // Index of the currently executing advisor
@Override
public Object proceed() throws Throwable {
currentAdvisorIndex++; // Move to the next advisor
if (currentAdvisorIndex < advisors.size()) {
// Execute the Advice of the next advisor
Advisor advisor = advisors.get(currentAdvisorIndex);
return advisor.getAdvice().invoke(this); // Recursive chain execution
} else {
// All advisors have been executed β invoke the original method
return methodProxy.invoke(target, args);
}
}
}
Chain Execution Flow
proceed() is called
β
currentAdvisorIndex++
β
index < advisors.size() ?
ββ Yes β advisor.getAdvice().invoke(this) β Execute advice
β β
β Recursively call proceed()
β β
β Next advisor or original method
ββ No β methodProxy.invoke(target, args) β Execute original method
2. MethodSignature
: Optimizing Method Metadataβ
Lazy Calculation and Caching Strategy
public class MethodSignature implements Signature {
private final Method method;
private volatile String cachedToString; // Cache for string representation
private volatile String cachedLongName; // Cache for long name
@Override
public String toLongName() {
String local = cachedLongName;
if (local == null) { // null on first call
synchronized (this) { // synchronized block
if (cachedLongName == null) { // double-checked locking
cachedLongName = method.toGenericString();
}
local = cachedLongName;
}
}
return local;
}
}
Performance Optimization Techniques
volatile
Fields: Guarantees memory visibility.- Double-Checked Locking: Minimizes synchronization overhead.
- Lazy Initialization: Calculation occurs only when first used.
- Local Variable Usage: Avoids redundant
volatile
reads.
Integration Mechanism with DI Containerβ
1. BeanPostProcessor
Registration Timingβ
Registration Strategy
private void registerBeanPostProcessors() {
List<BeanPostProcessor> allBeanPostProcessors = beanFactory.getAllBeans(BeanPostProcessor.class);
for (BeanPostProcessor beanPostProcessor : allBeanPostProcessors) {
beanFactory.addBeanPostProcessor(beanPostProcessor);
}
}
Execution Timing: After infrastructure bean initialization is complete, but right before application bean initialization begins.
2. AOP Integration in the Bean Creation Lifecycleβ
AOP's Intervention During Bean Creation
// Inside DefaultListableBeanFactory's bean creation process
public Object createBean(BeanDefinition bd) {
// 1. Create instance
Object instance = instantiateBean(bd);
// 2. Inject dependencies
injectDependencies(instance, bd);
// 3. Execute BeanPostProcessors (including AOP)
for (BeanPostProcessor processor : beanPostProcessors) {
instance = processor.postProcessAfterInitialization(bd.getName(), instance);
}
return instance;
}
3. Preserving Proxy and Original Object Metadataβ
Using CtorMeta
// Store constructor info when creating the original bean
private final Map<Object, CtorMeta> ctorCache = new IdentityHashMap<>();
// Use the same constructor info when creating the proxy
CtorMeta meta = container.lookupCtorMeta(bean);
return proxyFactory.createProxy(targetClass, bean, advisorRegistry, meta);
Performance Analysis and Optimizationβ
1. Time Complexity Analysisβ
Proxy Creation Decision Process
- Method Iteration: O(m) (m = number of public methods in the class)
- Advisor Matching: O(n) Γ O(p) (n = # of advisors, p = pointcut matching complexity)
- With Cache Hit: O(1) (leveraging
AdvisorRegistry
caching)
Method Interception Process
- Advisor Lookup: O(1) (on cache hit)
- Chain Execution: O(a) (a = number of applicable advisors)
- Original Method Invocation: O(1) (direct call via CGLIB
MethodProxy
)
2. Memory Usage Optimizationβ
Caching Strategies
// Per-method caching in AdvisorRegistry
private final Map<Method, List<Advisor>> cachedAdvisors = new ConcurrentHashMap<>();
// String representation caching in MethodSignature
private volatile String cachedToString;
private volatile String cachedLongName;
Memory Efficiency
ConcurrentHashMap
: Optimized for read-heavy operations.IdentityHashMap
: Fast lookups based on object identity.AtomicBoolean
: Prevents duplicate initializations.volatile
Caching: Provides lazy initialization and memory visibility.
3. CGLIB vs. JDK Dynamic Proxy Comparisonβ
Feature | CGLIB | JDK Dynamic Proxy |
---|---|---|
Base Technology | Bytecode Generation | Reflection |
Interface Required | No | Yes |
Basis | Class Inheritance | Interface Implementation |
Performance | Faster (direct invocation) | Slower (reflection overhead) |
final Methods | Cannot be intercepted | N/A |
Constructor Support | Yes | No |
Why Sprout Chose CGLIB
- Interface Independence: Does not force business classes to implement interfaces.
- Performance Priority: Optimized for performance via
MethodProxy
's direct invocation. - Constructor Support: Integrates naturally with DI.
Comparison with Spring AOPβ
Architectural Differencesβ
Aspect | Spring AOP | Sprout AOP |
---|---|---|
Proxy Creation Point | BeanPostProcessor | BeanPostProcessor |
Infra Initialization | BeanFactoryPostProcessor | PostInfrastructureInitializer |
Aspect Scanning | Integrated with component scan | Separate Reflections scan |
Advisor Registration | Automatic + BeanDefinition | Explicit Registry |
Proxy Factory | ProxyFactory (complex) | CglibProxyFactory (simple) |
Method Chain | ReflectiveMethodInvocation | MethodInvocationImpl |
Design Philosophy Differencesβ
Spring AOP
- Complex and flexible proxy creation strategies.
- Supports various proxy types (JDK + CGLIB).
- Manages metadata based on
BeanDefinition
.
Sprout AOP
- Simple and clear proxy creation strategy.
- Supports only CGLIB to reduce complexity.
- Improves readability with an explicit registry pattern.
Extensibility and Customizationβ
1. Implementing a New ProxyFactory
β
@Component
public class CustomProxyFactory implements ProxyFactory, InfrastructureBean {
@Override
public Object createProxy(Class<?> targetClass, Object target, AdvisorRegistry registry, CtorMeta meta) {
// Use JDK Dynamic Proxies or another proxy technology
return createCustomProxy(targetClass, target, registry);
}
}
2. Custom PostInfrastructureInitializer
β
@Component
public class CustomAopInitializer implements PostInfrastructureInitializer {
@Override
public void afterInfrastructureSetup(BeanFactory beanFactory, List<String> basePackages) {
// Custom AOP initialization logic
initializeCustomAspects();
}
}
3. Extending the BeanPostProcessor
Chainβ
@Component
@Order(100) // Execute after AspectPostProcessor
public class CustomPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(String beanName, Object bean) {
// Additional post-processing logic
return enhanceBean(bean);
}
}
Debugging and Monitoringβ
1. Verifying AOP Applicationβ
// Logging during proxy creation in AspectPostProcessor
if (needsProxy) {
System.out.println("Applying AOP proxy to bean: " + beanName + " (" + targetClass.getName() + ")");
// ...
}
2. Tracking Advisor Registration Statusβ
// Logging after advisor registration in AspectPostProcessor
System.out.println(aspectClass.getName() + " has " + advisorsForThisAspect.size() + " advisors: " + advisorsForThisAspect);
System.out.println("advisorRegistry#getAllAdvisors()" + advisorRegistry.getAllAdvisors());
3. Monitoring Method Interceptionβ
// Logging when interception occurs in BeanMethodInterceptor
if (!applicableAdvisors.isEmpty()) {
System.out.println("Intercepting method: " + method.getName() + " with " + applicableAdvisors.size() + " advisors");
}
Security Considerationsβ
1. Limitations of CGLIB-Based Proxiesβ
Security Constraints
final
Classes: Cannot be proxied by CGLIB.final
Methods: Cannot be overridden and thus cannot be intercepted.private
Methods: Not accessible from the proxy.- Constructor Invocation: The original object's constructor is called twice.
2. Enhancing Permission Checksβ
// Permission check before proxy creation in AspectPostProcessor
if (needsProxy && !hasProxyPermission(targetClass)) {
throw new SecurityException("Proxy creation not allowed for: " + targetClass.getName());
}
Sprout's AOP and DI/IoC integration system is designed to simplify Spring's complex proxy creation mechanism for educational purposes, while clearly demonstrating the core principles of how AOP works in practice.
Through infrastructure-first initialization, the PostInfrastructureInitializer
pattern, the BeanPostProcessor
chain, and CGLIB-based proxy creation, it provides a transparent and efficient AOP integration.
The design, which considers extensibility and ease of debugging, allows developers to easily understand and customize the internal workings of AOP.