🪞 Reflection, Annotations & Modules · Advanced

Dynamic proxies in Java

java.lang.reflect.Proxy and how frameworks add behavior.

🧩 The mysteryYou call greeter.greet("Ana"), and it works. But there's no class anywhere in your code that implements Greeter. Who answered?

An object built at runtime

**java.lang.reflect.Proxy creates, at runtime, an object that implements the interfaces you choose. Every call on it goes to your InvocationHandler, which receives the proxy, the Method being called and its arguments**, and decides what happens.

Object invoke(Object proxy,
              Method method,
              Object[] args)
🔮 Predict it

A calculator with no class

What does this print?

import java.lang.reflect.Proxy;
interface Calc { int add(int a, int b); }
void main() {
    var c = (Calc) Proxy.newProxyInstance(
        Calc.class.getClassLoader(),
        new Class<?>[] { Calc.class },
        (p, m, args) -> (int) args[0] * 10);
    System.out.println(c.add(2, 3));
}
  1. 5
  2. 20
  3. Compile error
Show the answer

There is no Calc implementation at all. The proxy routes add(2, 3) to the handler, which returns the first argument times 10: 20.

Wrapping a real object

Usually the handler forwards to a real object and adds something around the call: logging, timing, a transaction, a security check. Your class never knows.

InvocationHandler h = (p, m, args) -> {
    log("-> " + m.getName());
    return m.invoke(realRepo, args);
};
⚠️ The trap

Interfaces only

JDK proxies work only for interfaces. Passing a class fails at runtime with **IllegalArgumentException ("... is not an interface"). To proxy concrete classes, frameworks generate subclasses** with bytecode libraries like ByteBuddy.

class Repo { }
Proxy.newProxyInstance(loader,
    new Class<?>[] { Repo.class }, h);
// IllegalArgumentException
🤔 Think first

The missing transaction

In Spring, placeAll() calls this.place(), which is @Transactional. No transaction starts. Why?

Think about it, then reveal the answer

The transaction logic lives in the proxy wrapped around the bean. A call through **this goes straight to the real object and bypasses the proxy**. Self-calls never get the proxy's extras.

💼 In the real world

Proxies everywhere

Spring's @Transactional and @Cacheable work through proxies around your beans. HTTP client libraries like Retrofit turn an annotated interface into a working client: every method call becomes an HTTP request inside an InvocationHandler.

Key takeaways

  1. JDK proxies work only for interfaces
  2. Every call goes to InvocationHandler.invoke(proxy, method, args)
  3. Class-based proxies need bytecode libraries like ByteBuddy
  4. Self-calls via this bypass the proxy

💡 A proxy is a receptionist: every request goes through them, and they can log it, check badges, or forward it to the real person.

🤯 Did you know?

With Retrofit you write only an interface; the library hands back a java.lang.reflect.Proxy whose handler turns each method call into an HTTP request.

Practice questions

What does this print?

import java.lang.reflect.Proxy;
interface Greeter { String greet(String n); }
void main() {
    var g = (Greeter) Proxy.newProxyInstance(
        Greeter.class.getClassLoader(),
        new Class<?>[] { Greeter.class },
        (p, m, a) -> m.getName() + ":" + a[0]);
    System.out.println(g.greet("Ana"));
}
  1. Compile error
  2. Hello, Ana
  3. greet:Ana
  4. Throws UnsupportedOperationException
Check your answer

greet:Ana. There is no Greeter implementation class at all: the proxy forwards greet("Ana") to the handler, which returns the method name and first argument.

What does this print?

import java.lang.reflect.Proxy;
class Repo { }
void main() {
    Object o = Proxy.newProxyInstance(
        Repo.class.getClassLoader(),
        new Class<?>[] { Repo.class },
        (p, m, a) -> null);
    System.out.println("created");
}
  1. Compile error
  2. Throws IllegalArgumentException
  3. created
  4. Throws ClassCastException
Check your answer

Throws IllegalArgumentException. Proxy.newProxyInstance only accepts interfaces. Passing a class fails at runtime with IllegalArgumentException ("... is not an interface").

Proxies and reflection check things on every call. Next: MethodHandles, a typed and faster way to call methods dynamically.