🍃 Section 5 · Question #13

What is a proxy in Spring

In Spring, a Proxy is a dynamically generated wrapper object (surrogate/delegate) that wraps a target bean to intercept method invocations. It injects cross-cutting concerns—suc...


🟢 Junior Level

In Spring, a Proxy is a dynamically generated wrapper object (surrogate/delegate) that wraps a target bean to intercept method invocations. It injects cross-cutting concerns—such as transactions, security, caching, and logging—before, after, or around the actual business logic execution, without modifying the target class’s source code.

Caller (e.g., Controller)
          │
          ▼
┌────────────────────────────────────────────────────────┐
│                   Dynamic Proxy Object                 │
│                                                        │
│  1. Security Check    ──> Validate roles / permissions │
│  2. Transaction Begin ──> Open JDBC connection         │
│  3. Cache Check       ──> Return cached data if hit    │
│                                                        │
│         │                                              │
│         ▼ [Delegates to Target]                        │
│  ┌──────────────────────────────────────────────┐      │
│  │           Target Bean Instance               │      │
│  │                                              │      │
│  │   public Order createOrder(...) {            │      │
│  │       // Pure Business Logic                 │      │
│  │   }                                          │      │
│  └──────────────────────────────────────────────┘      │
│         │                                              │
│         ▼ [Returns execution to Proxy]                 │
│                                                        │
│  4. Transaction Commit ──> Commit or Rollback on error │
│  5. Cache Update       ──> Store calculated result     │
│  6. Audit Logging      ──> Log execution metrics       │
└────────────────────────────────────────────────────────┘
          │
          ▼
Returns Result to Caller

Why Spring Uses Proxies

Proxies enable Aspect-Oriented Programming (AOP) and maintain the Single Responsibility Principle (SRP). Business services remain completely agnostic of infrastructure concerns:

  • @Transactional: Opens a database connection, begins a transaction, commits on success, and triggers rollback on unhandled exceptions.
  • @Async: Submits method execution to a background TaskExecutor thread pool and returns a CompletableFuture or void immediately to the caller.
  • @Cacheable: Checks whether the requested data is already stored in Caffeine, Redis, or Ehcache before calling the method; stores the return value on completion.
  • @PreAuthorize / @Secured: Inspects the active SecurityContext to verify the caller has required roles/authorities before allowing method entry.

Practical Example

package com.example.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Transactional
    public void createOrder(Long userId, Long productId) {
        // Business logic execution
        System.out.println("Executing business logic in OrderService target instance");
    }
}

When another component injects OrderService:

@RestController
public class OrderController {

    // Spring injects the generated PROXY object, NOT the raw OrderService instance!
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
        System.out.println("Injected class: " + orderService.getClass().getName());
        // Prints: com.example.service.OrderService$$SpringCGLIB$$0
    }
}

🟡 Middle Level

The Two Proxy Mechanisms in Spring

Spring provides two dynamic proxy generation engines: JDK Dynamic Proxies and CGLIB (Code Generation Library) Proxies.

                         ┌────────────────────────────────────────┐
                         │   How Spring Generates Dynamic Proxies │
                         └───────────────────┬────────────────────┘
                                             │
                    ┌────────────────────────┴────────────────────────┐
                    ▼                                                 ▼
      ┌───────────────────────────┐                     ┌───────────────────────────┐
      │     JDK Dynamic Proxy     │                     │        CGLIB Proxy        │
      ├───────────────────────────┤                     ├───────────────────────────┤
      │ • Built into Java SE      │                     │ • Repackaged in Spring    │
      │ • java.lang.reflect.Proxy │                     │ • Bytecode enhancement    │
      │ • Interface-based         │                     │ • Subclass-based          │
      │ • Implements interfaces   │                     │ • Extends target class    │
      └───────────────────────────┘                     └───────────────────────────┘
Feature JDK Dynamic Proxy CGLIB Proxy
Underlying Mechanism java.lang.reflect.Proxy & InvocationHandler Bytecode generation via ASM / ByteBuddy (repackaged in org.springframework.cglib)
Proxy Generation Strategy Generates a new class implementing the target’s interfaces Generates a runtime subclass extending the target class
Target Class Requirement Must implement at least one interface Target class must not be final
Method Constraints Can only intercept interface methods Cannot intercept final or private methods
Concrete Type Injection ❌ Fails (BeanNotOfRequiredTypeException) ✅ Supported (proxy is an instanceof target class)
Generated Class Name jdk.proxy2.$Proxy42 OrderService$$SpringCGLIB$$0
Invocation Performance Direct reflection call through InvocationHandler Bytecode dispatch using MethodInterceptor (comparable speed on modern JVMs)

Why Spring Boot Defaults to CGLIB (proxy-target-class = true)

Historically, Spring Framework defaulted to JDK Dynamic Proxies whenever a target bean implemented at least one interface. If a bean did not implement any interface, Spring fell back to CGLIB.

However, starting with Spring Boot 2.0+ (and continued through Spring Boot 3.x), the configuration property:

spring.aop.proxy-target-class=true

is set to true by default.

The Type Mismatch Problem with JDK Proxies

Consider an interface and its implementation:

public interface OrderService {
    void process();
}

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    @Transactional
    public void process() { ... }
}

If Spring uses JDK Dynamic Proxy, the generated proxy class looks conceptually like this:

// JDK Proxy implements the interface, but does NOT extend OrderServiceImpl!
public final class $Proxy42 extends java.lang.reflect.Proxy implements OrderService { ... }

If a developer attempted to inject the concrete class:

@Autowired
private OrderServiceImpl orderService; // ❌ CRASHES ON STARTUP!

Spring failed with: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'orderServiceImpl' is expected to be of type 'com.example.OrderServiceImpl' but was actually of type 'jdk.proxy2.$Proxy42'

Because CGLIB generates a subclass (OrderServiceImpl$$SpringCGLIB$$0 extends OrderServiceImpl), it implements all interfaces and is an instanceof OrderServiceImpl. This eliminated countless injection bugs and made interfaces optional for single-implementation beans.


🔴 Senior Level

Internal Architecture: The Interceptor Chain

Within Spring AOP, all proxy instances implement the internal org.springframework.aop.framework.Advised interface. This allows Spring to inspect and configure advisors, advices, and pointcuts dynamically at runtime.

When a client invokes a method on a proxied bean:

  1. The proxy intercepts the call and passes it to the callback handler (DynamicAdvisedInterceptor in CGLIB or JdkDynamicAopProxy in JDK).
  2. The proxy queries its AdvisedSupport configuration to retrieve all Advisors whose Pointcuts match the called method.
  3. The advisors are converted into an ordered list of interceptors (List<MethodInterceptor>).
  4. An instance of ReflectiveMethodInvocation (or CglibMethodInvocation) is instantiated.
  5. The proceed() method walks through the chain using the Chain of Responsibility pattern:
Caller
  │
  ▼
OrderService$$SpringCGLIB$$0.createOrder()
  │
  ▼
CglibAopProxy.DynamicAdvisedInterceptor.intercept()
  │
  ▼
ReflectiveMethodInvocation.proceed():
  │
  ├──> [1] SecurityInterceptor.invoke()
  │          └── Checks roles -> calls proceed()
  │
  ├──> [2] TransactionInterceptor.invoke()
  │          ├── TransactionManager.getTransaction() [Opens Connection, sets autoCommit=false]
  │          ├── calls proceed()
  │          ├── On Success: TransactionManager.commit()
  │          └── On Throwable: TransactionManager.rollback()
  │
  ├──> [3] CacheInterceptor.invoke()
  │          ├── Checks cache store -> if hit, returns cached value early
  │          ├── calls proceed()
  │          └── Updates cache with return value
  │
  └──> [4] Target Method Invocation (via Method.invoke / FastClass)
             └── orderServiceTarget.createOrder(userId, productId)

Critical Production Traps and Limitations

Because Spring AOP relies on runtime proxies rather than compile-time or load-time bytecode weaving (like AspectJ), it is subject to fundamental Java language constraints.

1. The final Method Silent Trap (CGLIB)

CGLIB generates a subclass and overrides target methods. In Java bytecode, a method marked final cannot be overridden.

@Service
public class PaymentService {

    @Transactional
    public final void processPayment(BigDecimal amount) {
        // ❌ SILENT FAILURE IN SPRING BOOT!
        // CGLIB cannot override final methods in PaymentService$$SpringCGLIB$$0.
        // The JVM resolves the call directly to PaymentService.processPayment().
        // TransactionInterceptor is NEVER executed!
    }
}

[!WARNING] The compiler produces no error, and Spring Boot starts up without throwing any exception. However, processPayment executes completely outside of any transactional context. If an exception occurs, database updates will not roll back.

2. The Self-Invocation Trap

When a method within a bean calls another method on the same bean using this:

@Service
public class OrderService {

    public void processOrder(Order order) {
        // ❌ Internal call via 'this' bypasses the outer proxy!
        saveToDatabase(order);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveToDatabase(Order order) {
        // This transaction will NEVER be opened when invoked via processOrder()!
    }
}

The this reference always points to the raw, unproxied target object, completely bypassing the interceptor chain. To resolve this, see How to solve the self-invocation problem.

3. private and Package-Private Methods

  • JDK Dynamic Proxies can only proxy public interface methods.
  • CGLIB Proxies cannot override private methods because private methods are invisible to subclasses.
  • Annotations such as @Transactional, @Async, and @Cacheable placed on private methods are completely ignored by Spring AOP.

4. Constructor Invocation

During bean instantiation, the raw target instance’s constructor runs before any BeanPostProcessor can wrap it in a proxy. Calling an annotated method inside a constructor or @PostConstruct will execute on the unproxied instance without any interceptors.

Programmatic Inspection and Unwrapping

Spring provides AopUtils and AopProxyUtils to inspect and unwrap proxies during debugging, testing, or custom instrumentation:

import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;

public void inspectProxy(Object bean) throws Exception {
    // 1. Identify proxy type
    boolean isProxy = AopUtils.isAopProxy(bean);            // true if JDK or CGLIB
    boolean isCglib = AopUtils.isCglibProxy(bean);          // true if CGLIB subclass proxy
    boolean isJdk = AopUtils.isJdkDynamicProxy(bean);       // true if JDK interface proxy

    // 2. Extract original target class (strips $$SpringCGLIB$$)
    Class<?> targetClass = AopUtils.getTargetClass(bean);   // com.example.OrderService

    // 3. Extract the underlying unproxied singleton instance
    Object rawTarget = AopProxyUtils.getSingletonTarget(bean);

    // 4. Access the Advised interface to inspect active advisors
    if (bean instanceof Advised advised) {
        org.springframework.aop.Advisor[] advisors = advised.getAdvisors();
        for (org.springframework.aop.Advisor advisor : advisors) {
            System.out.println("Active Advisor: " + advisor.getAdvice().getClass().getName());
        }
    }
}

4 Tricky Questions

1. Why did Spring Boot 2.x/3.x switch spring.aop.proxy-target-class to true by default, and what are the trade-offs?

Answer: Spring Boot switched to CGLIB by default to eliminate BeanNotOfRequiredTypeException. In enterprise projects, developers frequently inject service classes directly (@Autowired private MyServiceImpl service) rather than declaring an interface for every single bean. Under JDK Dynamic Proxies, this fails because the proxy implements the interface but does not extend MyServiceImpl.

Trade-offs of defaulting to CGLIB:

  • Memory & Startup Overhead: CGLIB dynamically generates bytecode classes via ASM at startup, slightly increasing Metaspace consumption and startup time compared to lightweight JDK dynamic proxies.
  • Default Constructor Requirement: CGLIB proxies require a non-private no-arg constructor (or standard constructor) to instantiate the subclass proxy.
  • final Vulnerabilities: Any final class cannot be proxied at all (throws IllegalArgumentException: Cannot subclass final class), and final methods fail silently without proxying.

2. What happens under the hood if you place @Transactional on a final method in a Spring Boot application using CGLIB? Why does Spring not fail on startup?

Answer: When Spring Boot uses CGLIB, AnnotationAwareAspectJAutoProxyCreator scans beans for @Transactional annotations. It detects @Transactional on the method and wraps the bean in a CGLIB subclass proxy.

However, at the JVM bytecode level, a final method cannot be overridden by any subclass (INVOKEVIRTUAL dispatch rules). Therefore, the generated proxy subclass cannot override the final method to insert interceptor calls. When an external caller invokes the method on the proxy, the JVM executes the superclass implementation directly.

Spring does not fail on startup because method-level pointcut matching during proxy creation inspects method metadata, while class-level bytecode enhancement simply skips overriding final methods. The invocation executes normally on the target object, but completely bypasses the TransactionInterceptor, resulting in a silent transaction failure without error logs.

3. Can Spring AOP intercept private or protected methods? How does Spring handle them differently between JDK Proxy, CGLIB, and full AspectJ?

Answer:

  • private methods: Neither JDK Dynamic Proxy nor CGLIB can intercept private methods. JDK proxies only know about interface methods (which cannot be private prior to Java 9, and even in Java 9+ private interface methods cannot be called from outside the interface). CGLIB cannot override private methods because they are invisible to subclasses. Spring’s TransactionAttributeSourcePointcut explicitly ignores non-public methods. Only full AspectJ (via Compile-Time Weaving or Load-Time Weaving) can intercept private methods by directly instrumenting bytecode at call sites or method bodies.
  • protected methods: JDK proxies cannot intercept them (interfaces cannot declare protected methods). CGLIB can technically override protected methods, but standard Spring AOP annotations (like @Transactional) are deliberately configured by default to ignore protected and package-private methods, restricting interception exclusively to public methods unless configured via custom pointcuts or AspectJ.

4. If a bean is proxied by CGLIB, what happens if client code accesses its public fields directly instead of using getter methods?

Answer: Direct field access completely bypasses the proxy mechanism.

In CGLIB, the proxy object is a separate instance of the subclass that holds an internal reference to the actual target instance. The proxy subclass instance’s own fields are typically uninitialized (defaulting to null, 0, or false), while the actual business state resides inside the underlying target instance.

If a caller bypasses getter methods and directly reads a field (serviceProxy.myField), it reads the uninitialized field of the proxy subclass, not the field of the target instance! Interception and state delegation only occur when methods are invoked. For this reason, direct field access on Spring beans violates proxy encapsulation and leads to subtle NullPointerExceptions.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

A Spring Proxy is a dynamic runtime wrapper around a target bean that intercepts method invocations to execute cross-cutting concerns (AOP) like @Transactional, @Async, @Cacheable, and security checks via an ordered chain of MethodInterceptors before delegating to the target instance. Spring provides two proxy engines:

  1. JDK Dynamic Proxy: Interface-based, generated via java.lang.reflect.Proxy.
  2. CGLIB Proxy: Subclass-based, generated via bytecode enhancement (default in Spring Boot 2.x/3.x via spring.aop.proxy-target-class=true). Spring Boot defaults to CGLIB to allow direct injection of concrete class types without type-mismatch exceptions. Key limitations: proxies cannot intercept final methods (fails silently in CGLIB), private methods, or internal calls via this (self-invocation).

Core Comparison Matrix

Aspect JDK Dynamic Proxy CGLIB Proxy AspectJ (CTW / LTW)
Generation Type Runtime reflection Runtime bytecode subclass Compile-time / Classload bytecode modification
Target Requirement Must implement interface Class & methods cannot be final None (works on any class/method)
Supports Self-Invocation ❌ No ❌ No ✅ Yes (bytecode woven into class)
Supports private methods ❌ No ❌ No ✅ Yes
Spring Boot Default Disabled (false) Enabled (true) Optional (requires aspectjweaver)

Red Flags (DO NOT Say)

  • ❌ “Spring creates dynamic proxies for every single bean in the context.” (Proxies are only generated for beans that match active pointcuts: @Transactional, @Async, @Cacheable, @Scope(proxyMode), or custom @Aspects).
  • ❌ “CGLIB can proxy final methods because it operates on bytecode.” (Java bytecode verification strictly forbids overriding final methods in subclasses).
  • ❌ “Self-invocation works if you switch from JDK proxy to CGLIB.” (Self-invocation fails in both proxy types because this points directly to the target instance, completely bypassing the proxy wrapper).
  • ❌ “JDK Dynamic Proxy is always much slower than CGLIB.” (Modern JDKs and JIT compilers have optimized dynamic proxies significantly; Spring Boot chose CGLIB primarily for injection ergonomics, not raw dispatch performance).