AOP Proxies Under the Hood: From @Transactional to Runtime Interception
Prerequisites
- ›Article 2: BeanPostProcessor lifecycle
- ›Article 3: refresh() sequence and bean initialization
- ›Basic understanding of Java dynamic proxies
AOP Proxies Under the Hood: From @Transactional to Runtime Interception
When you annotate a method with @Transactional, Spring doesn't modify your class. Instead, it replaces your bean with a proxy — a runtime-generated wrapper that intercepts method calls, starts transactions before your code runs, and commits or rolls back afterward. This same proxy mechanism powers @Async, @Cacheable, Spring Security's @PreAuthorize, and every other annotation-driven cross-cutting concern. This article traces the proxy infrastructure from conceptual model to runtime bytecode.
The AOP Alliance Model: Advice, Pointcut, Advisor
Spring AOP is built on the AOP Alliance interfaces (from the org.aopalliance package), extended with Spring-specific abstractions. The key concepts are:
- Advice — What to do (the interceptor logic). Examples: start a transaction, check permissions, cache a result.
- Pointcut — Where to apply it (a predicate that matches join points). Examples: "all methods annotated with
@Transactional", "all public methods in theservicepackage". - Advisor — The combination of Advice + Pointcut. This is what gets registered with the proxy.
- AopProxy — The actual proxy object that wraps the target bean and dispatches calls through the advice chain.
ProxyFactory is the programmatic entry point for building proxy configurations:
spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java#L36
classDiagram
class ProxyConfig {
+proxyTargetClass: boolean
+optimize: boolean
+exposeProxy: boolean
}
class AdvisedSupport {
-targetSource: TargetSource
-advisors: List~Advisor~
+addAdvisor(Advisor)
+getInterceptorsAndDynamicInterceptionAdvice() List
}
class ProxyCreatorSupport {
-aopProxyFactory: AopProxyFactory
+createAopProxy() AopProxy
}
class ProxyFactory {
+getProxy() Object
}
class AopProxy {
<<interface>>
+getProxy() Object
}
class JdkDynamicAopProxy {
implements InvocationHandler
}
class ObjenesisCglibAopProxy {
Generates subclass
}
ProxyConfig <|-- AdvisedSupport
AdvisedSupport <|-- ProxyCreatorSupport
ProxyCreatorSupport <|-- ProxyFactory
AopProxy <|.. JdkDynamicAopProxy
AopProxy <|.. ObjenesisCglibAopProxy
AdvisedSupport holds the proxy configuration — the target object, the list of advisors, and interface information. ProxyCreatorSupport delegates to AopProxyFactory to create the actual AopProxy implementation. The factory makes the critical decision: JDK dynamic proxy or CGLIB subclass?
The Proxy Decision: JDK Dynamic Proxy vs CGLIB
The decision logic lives in DefaultAopProxyFactory.createAopProxy() — a surprisingly compact method:
spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java#L60-L76
flowchart TD
Start["createAopProxy(config)"]
Start --> Check1{"optimize || proxyTargetClass<br/>|| no user interfaces?"}
Check1 -->|No| JDK["JdkDynamicAopProxy<br/>(interface-based proxy)"]
Check1 -->|Yes| Check2{"targetClass is null<br/>|| is interface<br/>|| is Proxy class<br/>|| is lambda?"}
Check2 -->|Yes| JDK
Check2 -->|No| CGLIB["ObjenesisCglibAopProxy<br/>(subclass-based proxy)"]
The logic at line 61 checks three conditions that push toward CGLIB:
config.isOptimize()— rarely used directlyconfig.isProxyTargetClass()— theproxyTargetClass=trueflag!config.hasUserSuppliedInterfaces()— no interfaces were configured
Even when CGLIB is preferred, the factory falls back to JDK proxies for interfaces, existing proxies, and lambda classes (line 67–69), since CGLIB can't subclass these.
Spring Boot defaults to proxyTargetClass=true, meaning CGLIB proxies are used for virtually everything. This is a pragmatic choice: JDK proxies only expose methods declared on interfaces, which causes subtle bugs when code calls methods only on the concrete class. CGLIB proxies don't have this problem because they subclass the concrete type.
Tip: You can verify which proxy type Spring created for a bean by checking
AopUtils.isCglibProxy(bean)orAopUtils.isJdkDynamicProxy(bean). In debug logs, CGLIB proxies have class names containing$$SpringCGLIB$$.
AbstractAutoProxyCreator: Automatic Proxy Wrapping via BeanPostProcessor
Users rarely interact with ProxyFactory directly. Instead, Spring automatically wraps beans with proxies during the bean initialization phase we covered in Part 2. The mechanism is AbstractAutoProxyCreator — a SmartInstantiationAwareBeanPostProcessor that hooks into two phases of bean creation.
The first hook is in createBean(), at the resolveBeforeInstantiation() call:
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
This is the "short-circuit" path — if a BeanPostProcessor returns a non-null object from postProcessBeforeInstantiation(), it completely replaces the normal bean creation pipeline. This is used for special cases like scripted beans.
The more common path is postProcessAfterInitialization() — the final step of initializeBean() that we saw in Part 2. This is where AbstractAutoProxyCreator examines each bean, determines if any advisors apply, and wraps the bean with a proxy if needed.
sequenceDiagram
participant BF as BeanFactory
participant AAPC as AbstractAutoProxyCreator
participant DPF as DefaultAopProxyFactory
participant Proxy as Generated Proxy
BF->>AAPC: postProcessAfterInitialization(bean, "myService")
AAPC->>AAPC: wrapIfNecessary(bean, "myService", cacheKey)
AAPC->>AAPC: getAdvicesAndAdvisorsForBean()
Note right of AAPC: Find all Advisors whose<br/>Pointcuts match this bean
alt No matching advisors
AAPC-->>BF: return original bean
else Has matching advisors
AAPC->>AAPC: createProxy(bean, advisors)
AAPC->>DPF: createAopProxy(config)
DPF-->>AAPC: JdkDynamicAopProxy or CglibAopProxy
AAPC->>Proxy: aopProxy.getProxy()
AAPC-->>BF: return proxy (replaces original bean)
end
The key insight is that getAdvicesAndAdvisorsForBean() queries all registered Advisor beans in the container and checks their pointcuts against the target bean's class. For @Transactional, the advisor's pointcut matches methods annotated with @Transactional. For @Async, it matches methods annotated with @Async.
The concrete subclass you'll encounter most is AnnotationAwareAspectJAutoProxyCreator, which handles both Spring's native advisors and AspectJ-style @Aspect classes.
The Invocation Chain at Runtime
When a proxied method is called at runtime, the proxy doesn't call your method directly. Instead, it builds an invocation chain from all applicable interceptors:
For JDK proxies, JdkDynamicAopProxy implements InvocationHandler:
spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java
For CGLIB proxies, CglibAopProxy generates a subclass with callback interceptors:
spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java
Both implementations follow the same pattern: they obtain the chain of MethodInterceptor instances from AdvisedSupport, then invoke them in order via ReflectiveMethodInvocation (JDK) or CglibMethodInvocation (CGLIB). Each interceptor can:
- Do work before calling
proceed()(e.g., start a transaction) - Call
proceed()to invoke the next interceptor (or the target method) - Do work after
proceed()returns (e.g., commit the transaction) - Catch exceptions (e.g., roll back the transaction)
This is the classic Chain of Responsibility pattern, and it's what makes @Transactional + @Cacheable + @Async stackable on a single method.
MergedAnnotations: The Annotation Discovery Engine
How does Spring discover that a method has @Transactional when the actual annotation might be @TransactionalService — a custom meta-annotation that contains @Transactional? The answer is MergedAnnotations:
spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java#L31-L60
MergedAnnotations provides a unified view of annotations on a Java element (class, method, field), including:
- Directly present annotations — physically on the element
- Meta-annotations — annotations on the annotations (recursively)
@AliasForresolution — attribute aliasing between annotations and their meta-annotations
flowchart TD
Element["@PostMapping('/users')"]
Element --> Direct["Direct: @PostMapping"]
Direct --> Meta1["Meta: @RequestMapping(method=POST)"]
Meta1 --> Meta2["Meta: @Mapping"]
Element --> Merged["MergedAnnotations.from(method)"]
Merged --> Get["mergedAnnotations.get(RequestMapping.class)"]
Get --> Result["MergedAnnotation with merged attributes:<br/>path='/users', method=POST"]
Note1["@AliasFor resolves<br/>@PostMapping.value → @RequestMapping.path"]
This is what allows @GetMapping("/users") to work as a shortcut for @RequestMapping(path="/users", method=GET) — the MergedAnnotations system transparently merges the attributes across the meta-annotation hierarchy. The same system powers AOP pointcut matching: when checking if a method is @Transactional, Spring uses MergedAnnotations to find @Transactional even if it's buried inside a composed annotation.
Tip: If you're building custom annotations with
@AliasFor, test them withMergedAnnotations.from(element).get(YourAnnotation.class)to verify attribute merging works as expected. Subtle mistakes in@AliasFordeclarations fail silently.
Putting It All Together
The complete AOP proxy flow connects every piece we've covered across the series:
-
Part 3:
refresh()step 5 discovers@EnableTransactionManagementon a configuration class, which@ImportsProxyTransactionManagementConfiguration, registering aTransactionInterceptor(Advice) andTransactionAttributeSourceAdvisor(Advisor). -
Part 3:
refresh()step 6 registersAnnotationAwareAspectJAutoProxyCreatoras aBeanPostProcessor. -
Part 2: During step 11 (
finishBeanFactoryInitialization), when your@Servicebean is created,initializeBean()callspostProcessAfterInitialization()on every registeredBeanPostProcessor. -
This article:
AbstractAutoProxyCreator.postProcessAfterInitialization()finds that theTransactionAttributeSourceAdvisor's pointcut matches your bean (because it has@Transactionalmethods). It creates a CGLIB proxy wrapping your bean with the transaction interceptor. -
At runtime, calling a
@Transactionalmethod goes through the proxy, which invokes theTransactionInterceptor, which starts a transaction, calls your actual method, and commits or rolls back.
What's Next
We've now covered the four internal pillars: modules, IoC container, bootstrap sequence, and AOP proxies. In the next article, we'll move to the web layer — tracing a request through DispatcherServlet.doDispatch() and its reactive counterpart DispatcherHandler.handle(), and comparing how the same Strategy pattern produces a 1,400-line imperative servlet versus a 220-line reactive handler.