skip to content

Show how to use a JDK dynamic proxy to add logging or timing transparently around any interface (a transparent decorator).

level: middleimportance: should knowfreq 35%

answer

  1. Hold the real target, forward via method.invoke
  2. Generic factory: <T> T logged(Class<T>, T)
  3. Cast proxy with iface.cast(...)
  4. Unwrap InvocationTargetException.getCause()
  5. Self-calls inside target skip the proxy

basics

~10 s

Wrap the real object in a proxy whose InvocationHandler logs before and after forwarding each call with method.invoke(target, args). Callers use the same interface and never know logging was added.

solid answer

~50 s

A transparent decorator adds behavior without changing the caller. With a JDK proxy you hold a reference to the real target, create a proxy implementing the same interface, and in the handler you log (or time, or retry) and then forward via method.invoke(target, args). Because the proxy is cast to the interface, callers are unaware. A clean pattern is a small generic factory: <T> T logged(Class<T> iface, T target) that returns the proxy. Inside the handler, unwrap InvocationTargetException so the original exception surfaces, and consider skipping or specially handling Object methods. This is exactly how lightweight AOP works: the same handler can be reused for timing, auditing, caching, transaction begin/commit, or security checks. The main limitation is that the target must be reached through an interface, and self-calls inside the target won't go through the proxy.

code

java · 16 lines
java
static <T> T logged(Class<T> iface, T target) {
    return iface.cast(Proxy.newProxyInstance(
        iface.getClassLoader(),
        new Class<?>[]{ iface },
        (proxy, method, args) -> {
            long start = System.nanoTime();
            try {
                Object result = method.invoke(target, args);
                System.out.printf("%s -> %s (%d ns)%n",
                        method.getName(), result, System.nanoTime() - start);
                return result;
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
        }));
}

go deeper

for a junior

Can wrap one interface to print a message before forwarding the call.

for a middle

Writes a reusable generic proxy factory, forwards correctly, and unwraps reflective exceptions.

for a senior

Generalizes the handler to caching/retry/transactions and explains the self-invocation limitation and Object-method handling.

for a principal

Designs a composable interception pipeline, considers performance/thread-safety of shared handler state, and weighs proxy-based AOP against compile-time weaving.

## What 'transparent decorator' means The **Decorator pattern** wraps an object to add behavior while keeping the same type, so clients don't change. 'Transparent' means the wrapper is indistinguishable from the original from the caller's side — same interface, same usage. A JDK dynamic proxy is a generic way to build such a decorator **without writing a wrapper class per interface**. ## The ingredients - A **target**: the real object implementing some interface `T`. - An **InvocationHandler** that, for each call, performs the added behavior and then **forwards** to the target with `method.invoke(target, args)`. - A **proxy** created by `Proxy.newProxyInstance` and cast to `T`. ## A reusable generic factory ```java static <T> T logged(Class<T> iface, T target) { return iface.cast(Proxy.newProxyInstance( iface.getClassLoader(), new Class<?>[]{ iface }, (proxy, method, args) -> { long start = System.nanoTime(); try { Object result = method.invoke(target, args); System.out.printf("%s -> %s (%d ns)%n", method.getName(), result, System.nanoTime() - start); return result; } catch (InvocationTargetException e) { throw e.getCause(); // surface the real exception } })); } ``` Usage: ```java Repository repo = logged(Repository.class, new JdbcRepository()); repo.find(5); // logs name, result, timing ``` ## Why forwarding details matter - **`method.invoke(target, args)`** calls the real method reflectively. Its result is returned to the proxy's caller. - When the target throws, reflection wraps it in **`InvocationTargetException`**; catching and rethrowing `getCause()` keeps the caller's exception type correct. - `args` is `null` for no-arg methods — fine here since you just pass it straight through. ## Reuse for other cross-cutting concerns The same shape powers many concerns by swapping the body of `invoke`: - **Timing/metrics** (above). - **Auditing/logging** of who called what. - **Caching**: build a key from `method`+`args`, return a cached value if present. - **Retry**: loop around `method.invoke` on transient failures. - **Transactions**: begin before, commit/rollback after. - **Security**: check permissions on `method` before forwarding. This is the essence of lightweight AOP. ## Limitations to call out - **Interface required** — the target must be reachable as an interface type. - **Self-invocation bypasses the proxy**: if a target method internally calls another of its own methods directly (`this.other()`), that call does **not** go through the proxy, so the cross-cutting behavior is skipped. (This is the classic Spring self-invocation gotcha.) - **Object methods** flow through the handler too; for a pure logging decorator forwarding them to the target is usually fine.

  • Why does self-invocation inside the target escape the decorator?
    The proxy only intercepts calls made through the proxy reference. When a target method calls another of its own methods via this, it bypasses the proxy entirely, so the added behavior doesn't run — the same reason Spring AOP misses internal calls.
  • How would you turn this logging proxy into a caching proxy?
    In invoke, build a cache key from method plus args; if present in a Map, return the cached value without forwarding; otherwise call method.invoke(target, args), store the result, and return it.

saying these in an interview costs you the question

  • Forwarding to the proxy instead of the target (infinite recursion)
  • Letting InvocationTargetException leak to callers
  • Assuming self-invocation in the target is also intercepted
  • Writing one bespoke wrapper class instead of a reusable handler

context