Dynamic Proxies in Java

Naveen AnamaneniUpdated · 8 min read

A dynamic proxy is an object that the JVM creates at run time to implement one or more interfaces you choose. The proxy has no code of its own for the interface methods. Instead, every method call on it is sent to a single method, invoke, of an object called the InvocationHandler. The handler decides what happens: call the real object, log, measure time, check permissions, open a transaction, or answer the call itself. This is how Spring adds @Transactional behavior, how Mockito creates mocks, and how the JVM implements annotations. In this lesson, you will learn how to create a dynamic proxy, what its rules and limits are, and where it is used.

How It Works

Every call on the proxy goes through one invoke method

The API

MemberDescription
Proxy.newProxyInstance(loader, interfaces, handler)It is used to create a proxy object implementing the given interfaces.
InvocationHandler.invoke(proxy, method, args)It is called for every method call on the proxy; its return value becomes the call's result.
Proxy.isProxyClass(class)It tests whether a class is a dynamic proxy class.
InvocationHandler.invokeDefault(proxy, method, args)It calls an interface's default method on the proxy (Java 16+).

The three parameters of invoke:

ParameterMeaning
proxyThe proxy object the call was made on
methodA Method object for the interface method that was called
argsThe arguments, or null when the method has none

Example: A Logging Proxy

Let's see an example that wraps a service so every call is logged, without changing the service or its callers:

Px.java▶ Try it
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
 
public class Px {
    interface MarksService {
        int total(String student);
        void add(String student, int marks);
    }
 
    static class RealMarksService implements MarksService {
        public int total(String student) {
            return student.length() * 10;
        }
 
        public void add(String student, int marks) {
            if (marks < 0) {
                throw new IllegalArgumentException("negative marks");
            }
        }
    }
 
    static 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()
                + Arrays.toString(args));
            try {
                Object result = method.invoke(target, args);
                System.out.println("<- " + result);
                return result;
            } catch (InvocationTargetException e) {
                System.out.println("<- threw " + e.getCause());
                throw e.getCause();
            }
        }
    }
 
    public static void main(String[] args) {
        MarksService real = new RealMarksService();
        MarksService service = (MarksService) Proxy.newProxyInstance(
            MarksService.class.getClassLoader(),
            new Class<?>[] {MarksService.class},
            new LoggingHandler(real));
 
        System.out.println("total = " + service.total("Navika"));
        service.add("Srihitha", 40);
        try {
            service.add("Advitha", -1);
        } catch (IllegalArgumentException e) {
            System.out.println("caller caught: " + e.getMessage());
        }
        System.out.println(Proxy.isProxyClass(service.getClass()));
        System.out.println(service instanceof MarksService);
        System.out.println(service instanceof RealMarksService);
    }
}
Output
-> total[Navika]
<- 60
total = 60
-> add[Srihitha, 40]
<- null
-> add[Advitha, -1]
<- threw java.lang.IllegalArgumentException: negative marks
caller caught: negative marks
true
true
false

Explanation:

  1. newProxyInstance generates a new class at run time that implements MarksService, and returns an object of it. The cast to MarksService is safe because the proxy really implements that interface.
  2. service.total("Navika") looks like a normal call, but it lands in LoggingHandler.invoke with the Method for total and the argument array.
  3. The handler logs, calls the real object with method.invoke(target, args), logs again and returns the result. One handler covers every method of the interface, including ones added later.
  4. Exceptions: method.invoke wraps the real exception in InvocationTargetException, so the handler rethrows the cause. The caller then sees the same IllegalArgumentException it would get from the real object.
  5. The proxy is a MarksService but not a RealMarksService. It wraps the real object; it doesn't extend it.

Example: A Proxy with No Real Object

A handler doesn't have to forward calls anywhere. This proxy answers the interface's methods from a map, which is how some libraries turn configuration files into typed objects:

Px2.java▶ Try it
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
 
public class Px2 {
    interface Settings {
        String theme();
        int fontSize();
 
        default String summary() {
            return theme() + " at " + fontSize() + "px";
        }
    }
 
    public static void main(String[] args) {
        Map<String, Object> values = new HashMap<>();
        values.put("theme", "dark");
        values.put("fontSize", 14);
 
        Settings settings = (Settings) Proxy.newProxyInstance(
            Settings.class.getClassLoader(),
            new Class<?>[] {Settings.class},
            (proxy, method, a) -> {
                if (method.isDefault()) {
                    return InvocationHandler.invokeDefault(proxy, method, a);
                }
                return values.get(method.getName());
            });
 
        System.out.println(settings.theme());
        System.out.println(settings.fontSize());
        System.out.println(settings.summary());
    }
}
Output
dark
14
dark at 14px

Explanation: InvocationHandler has one abstract method, so a lambda can implement it. For theme() and fontSize(), the handler looks up the method's name in the map; the Integer 14 is unboxed to the int return type automatically. For the default method summary(), invokeDefault runs the interface's own code, which in turn calls theme() and fontSize() through the proxy.

Rules and Limits

RuleDetails
Interfaces onlyProxy can't proxy a class. Passing a class throws IllegalArgumentException: ... is not an interface
Object methodsCalls to toString, equals and hashCode also go to the handler; other Object methods, like getClass, don't
Return typesThe handler's result must fit the method's return type, or the caller gets ClassCastException (or NullPointerException for null into a primitive)
Checked exceptionsIf the handler throws a checked exception the interface method doesn't declare, the caller gets UndeclaredThrowableException
Self-callsWhen the real object calls its own methods, those calls don't pass through the proxy

To proxy a class that has no interface, frameworks generate a subclass at run time with a bytecode library such as Byte Buddy or CGLIB. That is why Spring can't apply @Transactional to final classes or final methods with that approach, and why a method calling another method of the same object skips the transaction logic.

Where Dynamic Proxies Are Used

UseHow
Spring AOP, @Transactional, @CacheableA proxy around your bean runs the extra behavior before and after each call
Mocking (Mockito)A proxy records calls and returns preset answers
Spring Data repositories, HTTP client interfacesYou write only an interface; a proxy implements it from method names and annotations
Remote callsA proxy turns a method call into a network request
AnnotationsThe object returned by getAnnotation is a proxy implementing the annotation interface

Common Mistakes

Calling method.invoke(proxy, args) inside the handler. That calls the proxy again, which calls the handler again, forever, until StackOverflowError. Invoke on the real target.

Not unwrapping InvocationTargetException. Callers then receive UndeclaredThrowableException instead of the real exception.

Forgetting toString, equals and hashCode. They reach the handler too; a handler that returns null for everything breaks them.

Trying to proxy a class. Only interfaces work with java.lang.reflect.Proxy.

Interview Corner

What is a dynamic proxy in Java?

A dynamic proxy is a class generated at run time by Proxy.newProxyInstance that implements a given list of interfaces. Every method call on the proxy instance is dispatched to the invoke(proxy, method, args) method of an InvocationHandler, which can add behavior before and after calling a real target with method.invoke, or produce the result itself. It works only with interfaces. Frameworks use it for AOP (transactions, caching, security), mocking, repository interfaces and remote calls.

What is the difference between JDK dynamic proxies and CGLIB-style proxies?

JDK dynamic proxies implement interfaces and are part of the standard library. Subclass-based proxies (CGLIB, Byte Buddy) generate a subclass of a concrete class at run time and override its methods, so they work without interfaces but can't handle final classes or final methods. Spring uses JDK proxies when a bean is exposed through interfaces and subclass proxies otherwise (Spring Boot defaults to subclass proxies).

Practice

  1. Easy: Write a TimingHandler that prints how many microseconds each call took, and wrap a List<String> with it.
  2. Harder: Write a handler that caches results: the first call of a method with given arguments goes to the real object; later calls with equal arguments return the stored result.

Quick check: Dynamic proxies

4 questions
1What does Proxy.newProxyInstance create?
2Where does every call on the proxy go?
3What happens if you pass a class instead of an interface?
4Inside the handler, what should method.invoke be called on?

Key Takeaways

Was this tutorial helpful?

Lesson 471 of 482