Dynamic Proxies
Create runtime proxies with java.lang.reflect.Proxy to implement cross-cutting concerns like logging and timing.
Overview
Key Points
- •Proxy.newProxyInstance(classLoader, interfaces[], handler) creates a dynamic proxy
- •The proxy implements all the specified interfaces
- •Every method call on the proxy is routed to handler.invoke(proxy, method, args)
- •handler.invoke() can delegate to the real object, add behavior, or return a fake value
- •method.invoke(target, args) inside the handler calls the real method
- •Dynamic proxies only work with interfaces; for class proxies use CGLIB or ByteBuddy
- •Spring's @Transactional wraps your service in a dynamic proxy that opens/commits transactions
- •JDK proxies are slightly slower than compiled proxies due to reflection; negligible for I/O-bound code
Examples
Logging proxy for any interface
import java.lang.reflect.*;
interface UserService {
String getUser(int id);
void createUser(String name);
}
class UserServiceImpl implements UserService {
public String getUser(int id) { return "User#" + id; }
public void createUser(String name) { System.out.println("Created: " + name); }
}
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() + " called");
long start = System.nanoTime();
Object result = method.invoke(target, args);
long elapsed = (System.nanoTime() - start) / 1_000;
System.out.println("<< " + method.getName() + " returned in " + elapsed + "us");
return result;
}
}
public class Main {
@SuppressWarnings("unchecked")
public static <T> T logged(T target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class[]{iface},
new LoggingHandler(target)
);
}
public static void main(String[] args) {
UserService service = logged(new UserServiceImpl(), UserService.class);
System.out.println(service.getUser(42));
service.createUser("Alice");
}
}The generic logged() method works for any interface. You can add logging to any service without touching its implementation. This is what Spring AOP does at a much larger scale.
Retry proxy: automatically retry on exception
import java.lang.reflect.*;
interface DataFetcher {
String fetch(String key);
}
public class RetryProxy {
private static int callCount = 0;
public static DataFetcher withRetry(DataFetcher target, int maxRetries) {
return (DataFetcher) Proxy.newProxyInstance(
DataFetcher.class.getClassLoader(),
new Class[]{DataFetcher.class},
(proxy, method, args) -> {
Exception last = null;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
last = (Exception) e.getCause();
System.out.println("Attempt " + (attempt + 1) + " failed: " + last.getMessage());
}
}
throw last;
}
);
}
public static void main(String[] args) throws Exception {
DataFetcher flaky = key -> {
callCount++;
if (callCount < 3) throw new RuntimeException("Service unavailable");
return "data-for-" + key;
};
DataFetcher resilient = withRetry(flaky, 5);
System.out.println(resilient.fetch("user:1")); // succeeds on 3rd attempt
}
}The InvocationHandler is written as a lambda because InvocationHandler is a functional interface. The retry logic is completely separate from the business code.
Common Mistakes
- Trying to proxy a class instead of an interface. Proxy.newProxyInstance only works with interfaces. To proxy concrete classes, use a library like CGLIB (used by Spring) or ByteBuddy.
- Not unwrapping InvocationTargetException in the handler. When method.invoke() throws, the real cause is wrapped in InvocationTargetException. Re-throw e.getCause() to propagate the original exception.
- Calling methods on the proxy object from inside the handler. This causes infinite recursion. Always call method.invoke(target, args) on the real target, not on the proxy parameter.
Note
For more flexible proxying (including classes, not just interfaces), look into ByteBuddy or ASM, which Spring Boot uses under the hood when CGLIB proxying is needed. For AOP-style cross-cutting concerns in your own project, Spring AOP with @Aspect is far more ergonomic than building proxies manually.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 1 exercises
Knowledge Check
Question 1 of 4