jongkwan.dev
Development · Essay №060

Spring AOP and Proxies

How Spring AOP attaches cross-cutting concerns through proxies, and where the self-invocation limitation comes from.

Jongkwan Lee2026년 3월 9일6 min read
Contents

Spring AOP wraps target objects in proxies at runtime to attach cross-cutting concerns as supplementary behavior.

Cross-cutting concerns and proxies

Transactions, logging, and security checks show up repeatedly across methods in every layer. Putting that shared behavior directly into business code buries the core logic under supporting code. Shared concerns scattered across many places are called cross-cutting concerns.

Aspect-Oriented Programming (AOP) separates those cross-cutting concerns into their own modules. AOP does not replace object-oriented programming (OOP); it supplements the parts OOP handles awkwardly. Spring AOP implements that separation with proxies.

A proxy is a stand-in object that wraps the original and inserts supplementary behavior before and after a call. When a client calls the proxy, the proxy runs the supplementary behavior and then calls the original. Functionality can be added without touching the core code.

AOP terminology

Working with AOP starts with agreeing on the names of its parts. The terms below are borrowed by Spring AOP from AspectJ.

TermMeaning
Join pointA point where advice can be applied. Spring AOP supports method execution only
PointcutAn expression selecting the join points where advice is actually applied
AdviceThe supplementary behavior to apply. Around, Before, After, and others differ in when they run
AspectA module bundling advice with a pointcut. Declared with @Aspect
AdvisorExactly one advice + one pointcut. A Spring AOP-specific term
WeavingInserting advice into the join points a pointcut selected

Weaving is classified by when it happens: compile time, load time, or runtime. Spring AOP uses runtime weaving, and its mechanism is the proxy. That is why Spring AOP's capabilities and limitations are exactly those of proxies.

Annotating a class with @Aspect lets you bundle a pointcut and advice declaratively.

java
@Aspect
@Component
public class LogAspect {
 
    @Around("execution(* hello.aop.order..*(..))")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        // before and after joinPoint.proceed() is where the supplementary behavior goes
        return joinPoint.proceed();
    }
}

The string in @Around is the pointcut, and the method body is the advice. The proceed() call is the point where the original method runs.

Two proxy technologies

How the proxy is created depends on whether the target has an interface. Spring supports both an interface-based and a concrete-class-based approach.

TechnologyBasisCondition
JDK dynamic proxyInterfaceAn interface is required. Creates a proxy implementing that interface
CGLIBConcrete classCreates a proxy by subclassing the target class

A JDK dynamic proxy intercepts calls through InvocationHandler, CGLIB through MethodInterceptor. Because the two intercept differently, handling them separately duplicates code. Spring abstracts both behind ProxyFactory so they are handled uniformly.

The proxyTargetClass option on ProxyFactory decides which one is used. False selects the JDK dynamic proxy, true selects CGLIB. With no interface, only CGLIB is possible regardless of the option.

Spring Boot has defaulted to proxyTargetClass=true since 2.0 (per the official Spring Boot documentation). The related configuration key is spring.aop.proxy-target-class. That means a concrete-class proxy is always built with CGLIB, even when an interface exists. To go back to JDK dynamic proxies, change the setting.

properties
spring.aop.proxy-target-class=false

CGLIB is the default because it allows injection by concrete class type. A JDK dynamic proxy is interface-based, so it cannot be cast to the concrete class. CGLIB's remaining constraint is that it cannot proxy final classes or final methods. Since final is rarely used on AOP targets, this seldom gets in the way in practice.

When the proxy is created

Proxy creation is the job of a BeanPostProcessor. Adding spring-boot-starter-aop registers an auto proxy creator bean called AnnotationAwareAspectJAutoProxyCreator. That bean post processor steps in just before an object is registered in the bean store.

The bean post processor collects every registered advisor and checks the target against their pointcuts. It tests the object's methods one by one against the pointcut, and creates a proxy if even one matches. If nothing matches, the original is registered as the bean. That is why beans picked up by component scanning get proxies without any extra configuration code.

The self-invocation limitation

The trap people hit most often in Spring AOP is self-invocation. Because it is proxy-based, AOP applies only when the call goes through the proxy. Calling your own method from inside the target object bypasses that proxy.

java
@Component
public class CallService {
 
    public void external() {
        internal(); // this.internal() - calls the original directly, not the proxy
    }
 
    @Transactional
    public void internal() {
        // no transaction is applied when entered through external()
    }
}

The internal() inside external() has no receiver in front of it, so it resolves to this.internal(). And this is the actual target object, not the proxy. Advice such as the @Transactional on internal() therefore does not apply.

The fix is to make the call go through the proxy, and it usually comes down to three options.

  • Self-injection: receive the proxied version of your own bean and call internal() through that reference.
  • Lazy lookup: fetch the proxy late through ObjectProvider or ApplicationContext and call it.
  • Restructuring: split internal() into a separate bean so the call becomes an external one.

All three achieve the same goal, but restructuring is the recommended direction. Turning an internal call into a call on an external bean makes the dependency visible and routes through the proxy naturally. AspectJ, which weaves directly into the code, has no self-invocation problem, but it requires load-time weaving configuration and JVM options, which is a heavier burden.

Summary

Spring AOP attaches cross-cutting concerns as supplementary behavior through proxies at runtime. A pointcut selects the targets, advice holds the supplementary behavior, and a bean post processor finds the advisor that bundles them and generates the proxy automatically. Proxies come in two forms, the interface-based JDK dynamic proxy and the class-based CGLIB, and Spring Boot uses CGLIB by default. Because it is proxy-based, AOP drops out on self-invocation, so splitting internal calls out structurally to force them through the proxy is the safer approach.