Dynamic Proxies in Java
A dynamic proxy implements an interface at runtime and routes every call through one handler, which is how cross cutting behaviour is added.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The idea
A proxy stands in for a real object and controls access to it. A dynamic proxy is generated at runtime for one or more interfaces, so a single handler can wrap any implementation without a class being written for each.
caller --> proxy --> InvocationHandler --> real object
logging, timing,
retry, transactionsA static proxy first
interface NoteService {
Note find(long id);
void save(Note note);
}
class LoggingNoteService implements NoteService {
private final NoteService delegate;
LoggingNoteService(NoteService delegate) {
this.delegate = delegate;
}
@Override public Note find(long id) {
System.out.println("find " + id);
return delegate.find(id);
}
@Override public void save(Note note) {
System.out.println("save " + note.title());
delegate.save(note);
}
}This works, and it does not scale. Every method must be written out, and a new class is needed for every interface you want to log.
The dynamic version
public class LoggingHandler implements InvocationHandler {
private final Object target;
public LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("-> " + method.getName());
long start = System.nanoTime();
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
throw e.getCause(); // unwrap, or callers see the wrapper
} finally {
long tookMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("<- " + method.getName() + " in " + tookMs + " ms");
}
}
}@SuppressWarnings("unchecked")
public static <T> T logged(Class<T> type, T target) {
return (T) Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[]{type},
new LoggingHandler(target));
}NoteService service = logged(NoteService.class, new DefaultNoteService());
service.find(42); // logged automatically, with no per method codeThe same handler now works for any interface. That is the whole advantage.
What Proxy.newProxyInstance needs
| Argument | Purpose |
|---|---|
| Class loader | Where to define the generated class |
| Interfaces | What the proxy will implement |
| Invocation handler | Where every call is routed |
A JDK dynamic proxy can only implement interfaces. To proxy a concrete class you need bytecode generation, which is what libraries such as those used by dependency injection frameworks do behind the scenes.
A retry proxy
public class RetryHandler implements InvocationHandler {
private final Object target;
private final int attempts;
public RetryHandler(Object target, int attempts) {
this.target = target;
this.attempts = attempts;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Throwable last = null;
for (int attempt = 1; attempt <= attempts; attempt++) {
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
last = e.getCause();
if (!(last instanceof IOException)) {
throw last; // only retry transient failures
}
Thread.sleep(100L * attempt); // simple back off
}
}
throw last;
}
}Annotation driven behaviour
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cached { }
public class CachingHandler implements InvocationHandler {
private final Object target;
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public CachingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method real = target.getClass().getMethod(method.getName(), method.getParameterTypes());
if (!real.isAnnotationPresent(Cached.class)) {
return method.invoke(target, args);
}
String key = method.getName() + Arrays.toString(args);
return cache.computeIfAbsent(key, ignored -> {
try {
return method.invoke(target, args);
} catch (Exception e) {
throw new IllegalStateException(e);
}
});
}
}The annotation is read from the implementation method, because the interface method may not carry it. This detail catches people out.
Composing proxies
NoteService service = logged(NoteService.class,
retrying(NoteService.class,
cached(NoteService.class, new DefaultNoteService())));Each proxy wraps the next, so behaviours stack in a defined order. This is exactly how declarative logging, caching, retries and transactions are layered in frameworks.
Things to watch
equals,hashCodeandtoStringalso arrive at the handler. Handle them, or comparisons behave strangely.- A
defaultinterface method is routed to the handler too, and invoking it on the target needs care. - A call made from one method of the target to another does not pass through the proxy, because the target holds no reference to it. This surprises people who expect an inner call to be logged.
- Each reflective call carries some overhead. It is small compared with I/O and significant in a tight loop.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("toString") && method.getParameterCount() == 0) {
return "Proxy for " + target;
}
return method.invoke(target, args);
}Where you meet them
- Declarative transactions and security checks.
- Repository interfaces implemented at runtime from method names.
- Remote call stubs, where the proxy performs the network request.
- Mocking libraries in tests.
- Lazy loading of expensive objects.
Common mistakes
- Trying to proxy a class instead of an interface with the JDK proxy.
- Not unwrapping
InvocationTargetException, so callers see the wrong exception. - Ignoring
equalsandhashCodeand breaking collections. - Expecting self invocation inside the target to be intercepted.
- Putting slow work in a handler that runs on every call.
Best practices
- Keep each handler to a single concern.
- Always unwrap
InvocationTargetException. - Handle the
Objectmethods explicitly. - Design against interfaces so proxying stays possible.
- Use an existing framework rather than building an aspect system by hand.
Practice
- Write a proxy that logs the name and duration of every method call.
- Why can a JDK dynamic proxy not wrap a class with no interface?
- Add a retry proxy that only retries a specific exception type.
- Explain why a call from one method of the target to another is not intercepted.
- Compose two proxies and show that the order changes the behaviour.
Conclusion
A dynamic proxy implements interfaces at runtime and funnels every call into one handler, which is how logging, retries, caching and transactions are added without touching the implementation. Interfaces are the price of admission, and unwrapping exceptions is the detail most often missed.