Magic in the Wild: How Java Giants like Spring, Hibernate, and Mockito use Dynamic Proxies

Published: (January 1, 2026 at 09:59 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Welcome to the second installment of the series Java Proxies Unmasked.
In the previous post we learned about the classic Proxy pattern, built a static proxy, and saw why it doesn’t scale. We then introduced dynamic proxies, which generate proxies at runtime and solve the N + 1 problem of static proxies.

@Transactional Onion (Spring)

The most common place you’ll encounter a proxy in the Spring Framework is with the @Transactional annotation.

  • To us, it’s a single line of code.
  • To Spring, it’s a signal to wrap the bean in a complex “Proxy Onion.”

When you call a method marked with @Transactional, you aren’t calling your code directly; you’re calling a proxy that manages the lifecycle of a database connection.

How the proxy is created

  1. Bean Post‑Processor – During the Context Refresh phase, Spring’s BeanPostProcessor puts your class through a multi‑step transformation.

  2. InfrastructureAdvisorAutoProxyCreator – When Spring scans for beans and finds a class with @Transactional, this component asks:

    Does this bean have a “Pointcut” match? (e.g., is it annotated with @Transactional?)
    What “Advice” should I apply? (In this case, TransactionInterceptor.)

The Creation Phase (The Wrapper)

Spring uses a ProxyFactory. Depending on your configuration and class structure, it chooses one of two paths:

Proxy typeWhen it’s used
JDK Dynamic ProxyThe service implements an interface (e.g., UserServiceImpl implements UserService). Spring creates a proxy using java.lang.reflect.Proxy.
CGLIB ProxyThe service is a concrete class with no interface. Spring uses CGLIB to generate a subclass at runtime.

Note: JDK Dynamic Proxies will be explored in the next post. CGLIB (an older technology) is being superseded by ByteBuddy in newer Spring versions.

When a client calls your service, it hits the proxy’s invoke() (JDK) or intercept() (CGLIB) method:

// Pseudo‑code for a JDK dynamic proxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. TransactionAspectSupport.invokeWithinTransaction(...)
    // 2. Execute the target method
    // 3. Commit / rollback as needed
}

The Secret Sauce: TransactionAspectSupport

The real “engine room” lives in TransactionAspectSupport.java, especially the invokeWithinTransaction method, where the try‑catch block that wraps your code resides.

This is also why self‑invocation doesn’t work. If a method inside UserService calls another @Transactional method via this.otherMethod(), the call never goes through the proxy, so the transaction logic is skipped.

Lazy Loading (Hibernate)

Hibernate uses proxies to solve a massive performance problem: lazy loading.

Imagine a User entity with a List<Order>. Loading every order each time you fetch a user would cripple the database. Hibernate defaults to lazy loading for @OneToMany and @ManyToMany associations, returning hollow objects whose data is fetched only when accessed.

How it works

  • Hibernate returns a proxy that extends the entity class.
  • The proxy is generated with ByteBuddy; for an Order class you might see a class named Order$HibernateProxy$Q4X0SwcL.

What the generated subclass contains

  • LazyInitializer field – stores the entity ID, a reference to the Session, and the lazy‑initialization logic.
  • Overridden accessor methods that trigger SQL queries when first accessed, then cache the result.

When you call a getter on this proxy, ByteBuddy intercepts the call, checks whether the data is loaded, and if not, hydrates the object by executing an SQL query.

The Secret Sauce: ByteBuddyProxyFactory

The factory class ByteBuddyProxyFactory.java in the Hibernate source configures how ByteBuddy builds the hollow subclass and hooks the getters/setters to the LazyInitializer.

Mockito Spies

A Mockito spy wraps a real object. Unlike a mock (which starts with no behavior), a spy delegates method calls to the underlying object unless you explicitly stub them.

List list = new ArrayList<>();
List spyList = spy(list);   // Create a proxy around a real list

// Intercepted by the proxy, but delegates to the real method
spyList.size();                     // → 0

// Stub the proxy to return a custom value
doReturn(100).when(spyList).size();
spyList.size();                     // → 100

How Mockito creates a spy

Mockito.spy(myObject) uses ByteBuddy to generate a subclass at runtime (e.g., PaymentService$MockitoMock$cG76HxJ8v). The generated class copies the state from the real object, allowing the proxy to delegate calls while still being able to intercept and stub them.

TL;DR

FrameworkWhy it uses a proxyProxy technology
Spring (@Transactional)Manage transaction boundaries transparentlyJDK Dynamic Proxy or CGLIB (chosen by ProxyFactory)
HibernateLazy‑load associations to avoid unnecessary DB hitsByteBuddy‑generated subclass ($HibernateProxy$…)
MockitoProvide test doubles that can delegate or stub behaviorByteBuddy‑generated subclass ($MockitoMock$…)

MockMethodInterceptor

Every method in a Mockito‑generated subclass is overridden to call a single dispatcher: MockMethodInterceptor. This interceptor holds a registry of all stubbing instructions (e.g., when(...).thenReturn(...)).

When you call a method on a spy, the proxy decides:

“Do I run the real code, or do I return a canned answer?”

when() vs. doReturn()

  • Using

    when(spy.get(0)).thenReturn("foo");

    causes the real get(0) method to be invoked once before stubbing, which can throw IndexOutOfBoundsException on an empty list.

  • Using

    doReturn("foo").when(spy).get(0);

    talks directly to the proxy’s settings; the real method is never called.

The Secret Sauce: InvocationContainer

InvocationContainerImpl.java stores every method call made to the proxy, enabling later verification such as:

verify(spy).add(anyString());

Looking Ahead

We’ve uncovered the magic behind Spring, Hibernate, and Mockito proxies. In the next post we will build our very first dynamic proxy using only the standard JDK—no external libraries, no Maven dependencies—just pure Java.

Part 3: The Native Way will walk through writing an InvocationHandler. Get your IDE ready!

Back to Blog

Related posts

Read more »

Spring Security 시작하기 - 기본 설정과 인증

기본 설정 의존성 추가 Spring Security를 사용하려면 의존성만 추가하면 됩니다. 추가하는 것만으로 기본 보안이 활성화됩니다. Maven xml org.springframework.boot spring-boot-starter-security Gradle gradle imple...