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.

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, transactions

A 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 code

The same handler now works for any interface. That is the whole advantage.

What Proxy.newProxyInstance needs

ArgumentPurpose
Class loaderWhere to define the generated class
InterfacesWhat the proxy will implement
Invocation handlerWhere 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, hashCode and toString also arrive at the handler. Handle them, or comparisons behave strangely.
  • A default interface 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 equals and hashCode and 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 Object methods explicitly.
  • Design against interfaces so proxying stays possible.
  • Use an existing framework rather than building an aspect system by hand.

Practice

  1. Write a proxy that logs the name and duration of every method call.
  2. Why can a JDK dynamic proxy not wrap a class with no interface?
  3. Add a retry proxy that only retries a specific exception type.
  4. Explain why a call from one method of the target to another is not intercepted.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Reflection in Java

Reflection inspects and manipulates classes at runtime. It powers most frameworks and should be rare in application code.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.