Magic in the Wild: How Java Giants like Spring, Hibernate, and Mockito use Dynamic Proxies
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
-
Bean Post‑Processor – During the Context Refresh phase, Spring’s
BeanPostProcessorputs your class through a multi‑step transformation. -
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 type | When it’s used |
|---|---|
| JDK Dynamic Proxy | The service implements an interface (e.g., UserServiceImpl implements UserService). Spring creates a proxy using java.lang.reflect.Proxy. |
| CGLIB Proxy | The 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
Orderclass you might see a class namedOrder$HibernateProxy$Q4X0SwcL.
What the generated subclass contains
LazyInitializerfield – stores the entity ID, a reference to theSession, 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
| Framework | Why it uses a proxy | Proxy technology |
|---|---|---|
Spring (@Transactional) | Manage transaction boundaries transparently | JDK Dynamic Proxy or CGLIB (chosen by ProxyFactory) |
| Hibernate | Lazy‑load associations to avoid unnecessary DB hits | ByteBuddy‑generated subclass ($HibernateProxy$…) |
| Mockito | Provide test doubles that can delegate or stub behavior | ByteBuddy‑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 throwIndexOutOfBoundsExceptionon 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!