Dynamic Proxies in Java
A dynamic proxy is an object that the JVM creates at run time to implement one or more interfaces you choose. The proxy has no code of its own for the interface methods. Instead, every method call on it is sent to a single method, invoke, of an object called the InvocationHandler. The handler decides what happens: call the real object, log, measure time, check permissions, open a transaction, or answer the call itself. This is how Spring adds @Transactional behavior, how Mockito creates mocks, and how the JVM implements annotations. In this lesson, you will learn how to create a dynamic proxy, what its rules and limits are, and where it is used.
How It Works
The API
| Member | Description |
|---|---|
Proxy.newProxyInstance(loader, interfaces, handler) | It is used to create a proxy object implementing the given interfaces. |
InvocationHandler.invoke(proxy, method, args) | It is called for every method call on the proxy; its return value becomes the call's result. |
Proxy.isProxyClass(class) | It tests whether a class is a dynamic proxy class. |
InvocationHandler.invokeDefault(proxy, method, args) | It calls an interface's default method on the proxy (Java 16+). |
The three parameters of invoke:
| Parameter | Meaning |
|---|---|
proxy | The proxy object the call was made on |
method | A Method object for the interface method that was called |
args | The arguments, or null when the method has none |
Example: A Logging Proxy
Let's see an example that wraps a service so every call is logged, without changing the service or its callers:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class Px {
interface MarksService {
int total(String student);
void add(String student, int marks);
}
static class RealMarksService implements MarksService {
public int total(String student) {
return student.length() * 10;
}
public void add(String student, int marks) {
if (marks < 0) {
throw new IllegalArgumentException("negative marks");
}
}
}
static class LoggingHandler implements InvocationHandler {
private final Object target;
LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("-> " + method.getName()
+ Arrays.toString(args));
try {
Object result = method.invoke(target, args);
System.out.println("<- " + result);
return result;
} catch (InvocationTargetException e) {
System.out.println("<- threw " + e.getCause());
throw e.getCause();
}
}
}
public static void main(String[] args) {
MarksService real = new RealMarksService();
MarksService service = (MarksService) Proxy.newProxyInstance(
MarksService.class.getClassLoader(),
new Class<?>[] {MarksService.class},
new LoggingHandler(real));
System.out.println("total = " + service.total("Navika"));
service.add("Srihitha", 40);
try {
service.add("Advitha", -1);
} catch (IllegalArgumentException e) {
System.out.println("caller caught: " + e.getMessage());
}
System.out.println(Proxy.isProxyClass(service.getClass()));
System.out.println(service instanceof MarksService);
System.out.println(service instanceof RealMarksService);
}
}-> total[Navika]
<- 60
total = 60
-> add[Srihitha, 40]
<- null
-> add[Advitha, -1]
<- threw java.lang.IllegalArgumentException: negative marks
caller caught: negative marks
true
true
falseExplanation:
newProxyInstancegenerates a new class at run time that implementsMarksService, and returns an object of it. The cast toMarksServiceis safe because the proxy really implements that interface.service.total("Navika")looks like a normal call, but it lands inLoggingHandler.invokewith theMethodfortotaland the argument array.- The handler logs, calls the real object with
method.invoke(target, args), logs again and returns the result. One handler covers every method of the interface, including ones added later. - Exceptions:
method.invokewraps the real exception inInvocationTargetException, so the handler rethrows the cause. The caller then sees the sameIllegalArgumentExceptionit would get from the real object. - The proxy is a
MarksServicebut not aRealMarksService. It wraps the real object; it doesn't extend it.
Example: A Proxy with No Real Object
A handler doesn't have to forward calls anywhere. This proxy answers the interface's methods from a map, which is how some libraries turn configuration files into typed objects:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class Px2 {
interface Settings {
String theme();
int fontSize();
default String summary() {
return theme() + " at " + fontSize() + "px";
}
}
public static void main(String[] args) {
Map<String, Object> values = new HashMap<>();
values.put("theme", "dark");
values.put("fontSize", 14);
Settings settings = (Settings) Proxy.newProxyInstance(
Settings.class.getClassLoader(),
new Class<?>[] {Settings.class},
(proxy, method, a) -> {
if (method.isDefault()) {
return InvocationHandler.invokeDefault(proxy, method, a);
}
return values.get(method.getName());
});
System.out.println(settings.theme());
System.out.println(settings.fontSize());
System.out.println(settings.summary());
}
}dark
14
dark at 14pxExplanation: InvocationHandler has one abstract method, so a lambda can implement it. For theme() and fontSize(), the handler looks up the method's name in the map; the Integer 14 is unboxed to the int return type automatically. For the default method summary(), invokeDefault runs the interface's own code, which in turn calls theme() and fontSize() through the proxy.
Rules and Limits
| Rule | Details |
|---|---|
| Interfaces only | Proxy can't proxy a class. Passing a class throws IllegalArgumentException: ... is not an interface |
Object methods | Calls to toString, equals and hashCode also go to the handler; other Object methods, like getClass, don't |
| Return types | The handler's result must fit the method's return type, or the caller gets ClassCastException (or NullPointerException for null into a primitive) |
| Checked exceptions | If the handler throws a checked exception the interface method doesn't declare, the caller gets UndeclaredThrowableException |
| Self-calls | When the real object calls its own methods, those calls don't pass through the proxy |
To proxy a class that has no interface, frameworks generate a subclass at run time with a bytecode library such as Byte Buddy or CGLIB. That is why Spring can't apply @Transactional to final classes or final methods with that approach, and why a method calling another method of the same object skips the transaction logic.
Where Dynamic Proxies Are Used
| Use | How |
|---|---|
Spring AOP, @Transactional, @Cacheable | A proxy around your bean runs the extra behavior before and after each call |
| Mocking (Mockito) | A proxy records calls and returns preset answers |
| Spring Data repositories, HTTP client interfaces | You write only an interface; a proxy implements it from method names and annotations |
| Remote calls | A proxy turns a method call into a network request |
| Annotations | The object returned by getAnnotation is a proxy implementing the annotation interface |
Common Mistakes
Calling method.invoke(proxy, args) inside the handler. That calls the proxy again, which calls the handler again, forever, until StackOverflowError. Invoke on the real target.
Not unwrapping InvocationTargetException. Callers then receive UndeclaredThrowableException instead of the real exception.
Forgetting toString, equals and hashCode. They reach the handler too; a handler that returns null for everything breaks them.
Trying to proxy a class. Only interfaces work with java.lang.reflect.Proxy.
Interview Corner
What is a dynamic proxy in Java?
A dynamic proxy is a class generated at run time by Proxy.newProxyInstance that implements a given list of interfaces. Every method call on the proxy instance is dispatched to the invoke(proxy, method, args) method of an InvocationHandler, which can add behavior before and after calling a real target with method.invoke, or produce the result itself. It works only with interfaces. Frameworks use it for AOP (transactions, caching, security), mocking, repository interfaces and remote calls.
What is the difference between JDK dynamic proxies and CGLIB-style proxies?
JDK dynamic proxies implement interfaces and are part of the standard library. Subclass-based proxies (CGLIB, Byte Buddy) generate a subclass of a concrete class at run time and override its methods, so they work without interfaces but can't handle final classes or final methods. Spring uses JDK proxies when a bean is exposed through interfaces and subclass proxies otherwise (Spring Boot defaults to subclass proxies).
Practice
- Easy: Write a
TimingHandlerthat prints how many microseconds each call took, and wrap aList<String>with it. - Harder: Write a handler that caches results: the first call of a method with given arguments goes to the real object; later calls with equal arguments return the stored result.
Key Takeaways
Was this tutorial helpful?