🚀AdvancedReflection and Metaprogramming

Dynamic Proxies

Create runtime proxies with java.lang.reflect.Proxy to implement cross-cutting concerns like logging and timing.

Overview

A dynamic proxy is an object created at runtime that implements one or more interfaces, with all method calls routed through an InvocationHandler you provide. Unlike the static Proxy pattern (where you write the proxy class yourself), dynamic proxies generate the proxy class at runtime using the Reflection API. Dynamic proxies are the foundation of many Java frameworks: Spring AOP uses them for @Transactional and @Cacheable, MyBatis uses them to implement mapper interfaces without any code, and many mocking frameworks (Mockito) use them to generate test doubles. The two key classes are Proxy.newProxyInstance() which creates the proxy, and InvocationHandler whose invoke() method intercepts all calls.

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.

Loading editor…

Completed: 0 / 1 exercises

Knowledge Check

Question 1 of 4

What is required for Proxy.newProxyInstance to create a proxy?