Skip to main content

πŸ”— 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​

  1. Infrastructure-First Initialization: AOP-related infrastructure beans are initialized before application beans.
  2. PostProcessor Pattern: Transparent proxy creation is achieved through the BeanPostProcessor pattern.
  3. CGLIB-Based Proxies: Enables proxy creation even for classes that do not implement an interface.
  4. 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

  1. Guaranteed Order: Ensures the AOP infrastructure is ready before application beans are created.
  2. Dependency Resolution: Makes PostProcessors available at the time of application bean creation.
  3. 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 Advisors are registered before application beans are created.
  • Completes AOP setup before BeanPostProcessors are registered.

AspectPostProcessor: The Core Engine of AOP​

1. Dual-Role Architecture​

AspectPostProcessor performs two critical roles at different stages:

  1. During PostInfrastructureInitializer: Scans for Aspects and registers Advisors.
  2. 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

  1. Check Public Methods Only: private/protected methods are not AOP targets.
  2. Exclude Static Methods: Only instance methods can be intercepted.
  3. Early Exit: Create a proxy as soon as the first applicable Advisor is found.
  4. 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

  1. setSuperclass: Sets the original class as the superclass (for an inheritance-based proxy).
  2. setCallback: Sets the callback that will intercept all method calls.
  3. 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

  1. Early Branching: Invokes the original method immediately if no advisors apply.
  2. Cache Utilization: Leverages AdvisorRegistry's per-method advisor caching.
  3. Lazy Creation: MethodInvocation is created only when needed.
  4. 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

  1. volatile Fields: Guarantees memory visibility.
  2. Double-Checked Locking: Minimizes synchronization overhead.
  3. Lazy Initialization: Calculation occurs only when first used.
  4. 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

  1. ConcurrentHashMap: Optimized for read-heavy operations.
  2. IdentityHashMap: Fast lookups based on object identity.
  3. AtomicBoolean: Prevents duplicate initializations.
  4. volatile Caching: Provides lazy initialization and memory visibility.

3. CGLIB vs. JDK Dynamic Proxy Comparison​

FeatureCGLIBJDK Dynamic Proxy
Base TechnologyBytecode GenerationReflection
Interface RequiredNoYes
BasisClass InheritanceInterface Implementation
PerformanceFaster (direct invocation)Slower (reflection overhead)
final MethodsCannot be interceptedN/A
Constructor SupportYesNo

Why Sprout Chose CGLIB

  1. Interface Independence: Does not force business classes to implement interfaces.
  2. Performance Priority: Optimized for performance via MethodProxy's direct invocation.
  3. Constructor Support: Integrates naturally with DI.

Comparison with Spring AOP​

Architectural Differences​

AspectSpring AOPSprout AOP
Proxy Creation PointBeanPostProcessorBeanPostProcessor
Infra InitializationBeanFactoryPostProcessorPostInfrastructureInitializer
Aspect ScanningIntegrated with component scanSeparate Reflections scan
Advisor RegistrationAutomatic + BeanDefinitionExplicit Registry
Proxy FactoryProxyFactory (complex)CglibProxyFactory (simple)
Method ChainReflectiveMethodInvocationMethodInvocationImpl

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.