Deep Dive into JDK Dynamic Proxy Internals

In software architecture, cross-cutting concerns such as logging, security checks, and transaction management often need to be applied across multiple components. A naive approach involves manually inserting logic into every business method, leading to code duplication and tight coupling. For instance, if we need to audit execution before and after methods in hundreds of classes, modifying each class individually is unfeasible and prone to error.

The Static Proxy Approach

A preliminary solution is the Static Proxy pattern. By defining a common interface, we can create a proxy class that wraps the actual implementation, adding extra behavior without altering the original code.

Consider the following example with an interface and a concrete implementation:

public interface DataOperation {
    void execute(String query);
}

public class DatabaseOperation implements DataOperation {
    public void execute(String query) {
        System.out.println("Executing query: " + query);
    }
}

We then create a proxy class that holds a reference to the real object and decorates the method call:

public class OperationProxy implements DataOperation {
    private DataOperation target;

    public OperationProxy(DataOperation target) {
        this.target = target;
    }

    @Override
    public void execute(String query) {
        System.out.println("Start transaction");
        target.execute(query);
        System.out.println("Commit transaction");
    }
}

While this isolates the auxiliary logic, it introduces a new class for every interface implementation we wish to proxy. This lack of scalability leads us to Dynamic Proxies.

Implementing JDK Dynamic Proxy

JDK Dynamic Proxies allow us to generate proxy classes at runtime. The standard implementation involves the java.lang.reflect.Proxy class and the InvocationHandler interface.

1. Define the Interface:

public interface Service {
    void performTask();
}

2. Create the Target Implementation:

public class RealService implements Service {
    public void performTask() {
        System.out.println("Processing the core task logic.");
    }
}

3. Create the Invocation Handler:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimingHandler implements InvocationHandler {
    private Object target;

    public TimingHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--- Start Timer ---");
        Object result = method.invoke(target, args);
        System.out.println("--- Stop Timer ---");
        return result;
    }
}

4. Client Usage:

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        Service realService = new RealService();
        
        // Enable saving of generated proxy class files for debugging
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        Service proxyInstance = (Service) Proxy.newProxyInstance(
                realService.getClass().getClassLoader(),
                realService.getClass().getInterfaces(),
                new TimingHandler(realService)
        );

        proxyInstance.performTask();
    }
}

Source Code Analysis

The core of the mechanism lies in Proxy.newProxyInstance. This method performs three main tasks: validating inputs, obtaining the proxy class definition, and instantiating the proxy object.

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);
    
    // Security checks and interface validation omitted for brevity
    
    // Look up or generate the designated proxy class
    Class<?> cl = getProxyClass0(loader, interfaces);
    
    // Invoke its constructor with the invocation handler
    Constructor<?> cons = cl.getConstructor(constructorParams);
    return cons.newInstance(new Object[]{h});
}

Fetching the Proxy Class

The method getProxyClass0 checks a cache. If the proxy class for the given interfaces and loader does not exist, it creates one. The caching mechanism utilizes a WeakCache:

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    return proxyClassCache.get(loader, interfaces);
}

Generating the Bytecode

If the cache misses, the ProxyClassFactory is called to generate the class bytecode. This factory validates interfaces, determines the package name, and assigns a name (e.g., $Proxy0).

The critical step happens inside the apply method of ProxyClassFactory:

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    proxyName, interfaces, accessFlags);
    
return defineClass0(loader, proxyName,
                    proxyClassFile, 0, proxyClassFile.length);

ProxyGenerator.generateProxyClass constructs the bytecode for the proxy class. This class inherits from java.lang.reflect.Proxy and implements all the specified interfaces.

Why Only Interfaces?

The generated proxy class extends java.lang.reflect.Proxy. Since Java does not support multiple inheritance of classes, the proxy cannot inherit from a user-defined class. Therefore, JDK dynamic proxies can only proxy interfaces.

The Invocation Chain

When a method is called on the proxy instance, its internally routed to the invoke method of the InvocationHandler passed during construction. The generated proxy class contains a method similar to this:

public final void performTask() throws  {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

Here, super.h refers to the InvocationHandler (our TimingHandler), and m3 is the Method object corresponding to performTask. This reflection-based mechanism creates the "magic" of intercepting method calls.

Tags: java Dynamic Proxy reflection Design Patterns source code analysis

Posted on Thu, 02 Jul 2026 16:47:22 +0000 by liljester