Welcome to the second installment of the series: Java Proxies Unmasked. In the previous post, we learned about the classic Proxy pattern. We built a Static Proxy and realized that while it works for a simple example, it doesn't scale. Nobody wants to write a static proxy for every relevant class in their codebase.
We then teased the idea of Dynamic Proxies, which gave us a way to generate those proxies at runtime. They solved the N+1 problem plaguing static proxies and enabled us to write hundreds of them in few lines of code. But before we delve into the area further, we need to understand "why".
In this post, we're going to pull back the curtain from some of the widely used frameworks and libraries - Spring, Hibernate, and Mockito, and see how they use dynamic proxies to power features that a modern Java developer uses every day.
If you've ever felt that these frameworks do magic, today is the day we reveal their secrets.
Spring AOP: The @Transactional Onion
The most common place you'll encounter a proxy in Spring Framework is with the @Transactional annotation. To us, it's a single line of code. To Spring, it is a signal to wrap the bean in a complex "Proxy Onion". When you call a method marked with @Transactional, you aren't actually calling your code directly. You are calling a proxy that manages the lifecycle of a database connection.
To understand how this proxy is created, we have to look at the factory worker of the Spring container, the Bean Post-Processor. This processor puts your class through a multi-step transformation process during the Context Refresh phase.
Discovery & Pointcut Matching
As your application starts, Spring scans for beans. When it finds a class with @Transactional, a specialized component called the InfrastructureAdvisorAutoProxyCreator steps in. It asks two questions:
Does this bean have a "Pointcut" match? (e.g., Is it annotated with
@Transactional?)What "Advice" should I apply? (In this case, the
TransactionInterceptor).
The Creation Phase (The Wrapper)
Once Spring decides a proxy is needed, it uses a ProxyFactory. Depending on your configuration and class structure, it chooses one of two paths:
JDK Dynamic Proxy: If your service implements an interface (e.g.,
UserServiceImplimplementsUserService), Spring creates a proxy using the standard Javareflect.ProxyAPI.CGLIB Proxy: If your service is a concrete class with no interface, Spring uses CGLIB to generate a subclass of your bean at runtime.
We will take a look at JDK Dynamic Proxies in the next post. CGLIB (Code Generation Library) is an old technology, so we'll skip it in favor of ByteBuddy as we move further along.
The Call in Action
When a client calls your service, it doesn't hit your code first. It hits the Proxy's invoke() or intercept() methods. Here's how the sequence of events looks:
The Secret Sauce: TransactionAspectSupport
If you want to see the real "engine room," look at the TransactionAspectSupport.java in the Spring source code. Specifically, the invokeWithinTransaction method. This is where the actual try-catch block lives that wraps your code!
This is also why self-invocation doesn't work. If a method inside your UserService calls another @Transactional method in the same class using this.otherMethod(), the call never goes through the proxy "bouncer"—it stays inside the real object, and the transaction logic is skipped entirely!
Hibernate: When Loading is Lazy
Hibernate uses proxies to solve a massive performance problem: Lazy Loading. Imagine a User entity that has a List<Order>. A user may have placed hundreds of orders in the system, but we don't need them every time we fetch the user's details. Maybe we just need their name for an email campaign. If Hibernate loaded everything every time, your database would crawl to a halt.
Instead, Hibernate defaults to lazy loading for @OneToMany and @ManyToMany associations. It returns Hollow Objects whose data is fetched only when it is accessed by your code.
The birth of a Proxy
Hibernate uses Byte Buddy to generate a new class at Runtime that extends your entity. If you have an Order class, Hibernate generates a class that looks something like Order$HibernateProxy$Q4X0SwcL.
This generated subclass contains a special field: LazyInitializer. This is the "brain" of the proxy. It stores:
- The Entity ID, which is knows immediately
- A reference to the Hibernate Session, to fetch data later
- Overridden Accessor Methods. These trigger SQL queries when they are accessed.
- Lazy Initialization Logic.
The Call in Action
When you call an accessor method (like, a getter) on this proxy, Byte Buddy intercepts the call. It checks if the proxy object already as the data. If not, it "hydrates" the object (i.e. fetches the data) by running an SQL query.
The Secret Sauce: ProxyConfiguration
If you want to see where Hibernate configures this bytecode "sorcery", check out the ByteBuddyProxyFactory.java in the Hibernate source code. This is the factory that tells Byte Buddy exactly how to build that hollow subclass and how to hook the getters and setters to the LazyInitializer.
Mockito's Secret Agent: The spy()
A Mockito Spy is a special type of test double that wraps a real object. Unlike a regular mock that starts with no behavior, a spy delegates method calls to the actual underlying object unless you specifically stub (override) certain methods.
In simple words, if you don't tell the spy what to do, it simply passes the call through to the real method.
List<String> list = new ArrayList<>();
List<String> spyList = spy(list); // Create a proxy around a real list
// This call is intercepted by the proxy, but since
// we didn't stub it, it delegates to the REAL ArrayList.size()
spyList.size();
// Now we "stub" the proxy to lie to us
doReturn(100).when(spyList).size();
spyList.size(); // Returns 100
Subclassing the Real Thing
When you call Mockito.spy(myObject), Mockito doesn't just wrap the instance; it uses Byte Buddy to create a new class at runtime that extends your object's class.
If your class is PaymentService, Mockito creates PaymentService$MockitoMock$cG76HxJ8v. It then copies the state (the fields) from your real object into this new proxy instance. This is how you can spy on existing objects!
The Brain: MockMethodInterceptor
Every method in this generated subclass is overridden to call a single dispatcher: the MockMethodInterceptor. This interceptor holds a "Registry" of all your stubbing instructions (e.g., when(...).thenReturn(...)).
The Call in Action: To Real or Not to Real?
When you call a method on a spy, the proxy has to make a split-second decision: "Do I run the real code, or do I return a canned answer?"
The "Gotcha": when() vs. doReturn()
This proxy mechanism explains a famous Mockito trap.
If you use
when(spy.get(0)).thenReturn("foo"), the proxy actually calls the realget(0)method once before the stubbing is applied. if the list is empty, it throws anIndexOutOfBoundsException!If you use
doReturn("foo").when(spy).get(0), you are talking to the proxy's "settings" directly. The real method is never called.
The Secret Sauce: InvocationContainer
If you want to see how Mockito tracks these calls, look at InvocationContainerImpl.java. It’s the ledger that stores every method call made to the proxy, which is what allows you to later call verify(spy).add(anyString()).
In Conclusion
Whether it's Spring managing your transactions, Hibernate saving you memory, or Mockito helping you test, the secret ingredient is Interception.
Proxies allow these frameworks to "hook" into your method calls and execute their own logic without you ever having to change your source code. This is the essence of Declarative Programming: you declare the intent (via annotations), and the proxy handles the mechanics.
What’s Next?
We’ve seen the magic in action. Now, it’s time to become the magician. In the next post, we are going to build our very first dynamic proxy using nothing but the standard JDK. No libraries, no Maven dependencies—just pure Java.
Part 3: The Native Way is where we write our first InvocationHandler. Get your IDE ready!



Top comments (0)