Java Dynamic Proxy: Proxy, InvocationHandler & Practical Patterns
A Java dynamic proxy is a class generated at runtime that implements one or more interfaces, routing every method call through a single InvocationHandler. Frameworks like Spring AOP, Mockito, Hibernate, and JAX-RS client stubs all use dynamic proxies to add cross-cutting behaviour (logging, security checks, retry logic, caching, transaction management) without touching the original class. This article covers Proxy.newProxyInstance(), writing InvocationHandlers, and practical patterns with real examples.
What Is a Dynamic Proxy?
Imagine you want to add logging to every method of a UserService
A dynamic proxy solves this by letting you intercept all method calls through a single handler. The JVM generates the proxy class at runtime: you provide only the handler logic. Every call to every method on the proxy is funnelled through InvocationHandler.invoke()@Transactional@Cacheable
JDK dynamic proxies live in java.lang.reflect.Proxy
Creating a Proxy with Proxy.newProxyInstance()
Proxy.newProxyInstance()ClassLoaderInvocationHandlerObject
import java.lang.reflect.*;
// Step 1 — define the interface to proxy
public interface UserService {
String findById(long id);
void save(String name);
}
// Step 2 — real implementation
public class UserServiceImpl implements UserService {
@Override public String findById(long id) { return "User-" + id; }
@Override public void save(String name) { System.out.println("Saved: " + name); }
}
// Step 3 — create a simple logging proxy
UserService real = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
real.getClass().getClassLoader(), // ClassLoader
new Class[]{ UserService.class }, // interfaces to implement
(proxyObj, method, args) -> { // InvocationHandler as lambda
System.out.println("Calling: " + method.getName());
Object result = method.invoke(real, args); // delegate to real object
System.out.println("Returned: " + result);
return result;
}
);
proxy.findById(42);
// Calling: findById
// Returned: User-42
proxy.save("Alice");
// Calling: save
// Returned: null
InvocationHandlerargsObject[]nullInvocationHandler: Intercepting Method Calls
InvocationHandlerinvoke(Object proxy, Method method, Object[] args)ObjecttoString()equals()hashCode()Methodmethod.invoke(target, args)
A practical handler adds timing and structured logging around every call, including proper exception propagation. When method.invoke()InvocationTargetException
public class TimingHandler implements InvocationHandler {
private final Object target;
public TimingHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
Object result = method.invoke(target, args);
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.printf("[%s] completed in %d ms%n", method.getName(), ms);
return result;
} catch (InvocationTargetException e) {
// Unwrap so callers see the original exception, not InvocationTargetException
throw e.getCause();
}
}
}
UserService timed = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{ UserService.class },
new TimingHandler(new UserServiceImpl())
);
timed.findById(1); // [findById] completed in 0 ms
Practical Pattern: Retry Proxy
Network calls and database operations can fail transiently: a TCP timeout, a momentary connection pool exhaustion, or a primary failover. A retry proxy wraps any service interface and transparently re-attempts the call up to a configured number of times before letting the exception propagate. Because the proxy intercepts all methods uniformly, there is no need to add retry logic to every individual service method.
public class RetryHandler implements InvocationHandler {
private final Object target;
private final int maxAttempts;
private final long delayMs;
public RetryHandler(Object target, int maxAttempts, long delayMs) {
this.target = target;
this.maxAttempts = maxAttempts;
this.delayMs = delayMs;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Throwable lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
lastException = e.getCause();
System.err.printf("Attempt %d/%d failed for %s: %s%n",
attempt, maxAttempts, method.getName(), lastException.getMessage());
if (attempt < maxAttempts) {
Thread.sleep(delayMs * attempt); // back-off: 1x, 2x, 3x…
}
}
}
throw lastException; // all attempts exhausted
}
}
UserService resilient = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{ UserService.class },
new RetryHandler(new UserServiceImpl(), 3, 200)
);
Limitations of JDK Dynamic Proxies
JDK dynamic proxies are powerful but have important constraints you must understand before choosing them as a solution.
Interfaces only. The generated proxy class implements the listed interfaces; it cannot extend a concrete class. If you need to proxy a class that was not designed around an interface, you need CGLIB (which Spring uses automatically when no interface is available) or Byte Buddy.
Self-calls are not intercepted. If a proxied method calls another method on this@Transactional
Performance overhead. Each proxied call involves a reflection dispatch through Method.invoke()
toString, equals, hashCode. These Object
@EnableAspectJAutoProxy(proxyTargetClass = true)