Skip to Content
Course content

168: Dynamic Proxies with Reflection

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I worked with a developer named Sarah who was tasked with adding detailed execution timing to about forty different service classes in a legacy monolith. Her first instinct was the "correct" design pattern approach: the Decorator pattern. She started writing wrapper classes for every single service. By the end of the first day, she had created twelve nearly identical classes that did nothing but start a timer, call the original method, and stop the timer. She realized that if she kept going, she'd spend her entire sprint writing boilerplate code that provided zero business value. That's when I showed her Dynamic Proxies.

In Java, a dynamic proxy allows you to create an object at runtime that implements one or more interfaces. The magic is that you don't actually write the class for the proxy; Java generates it for you. This lets you intercept every single method call made to that object and route it through a single piece of logic. It's the secret sauce behind how frameworks like Spring handle @Transactional or @Cacheable without forcing you to write the plumbing yourself.

The Engine: InvocationHandler

To make a dynamic proxy work, you need a way to tell Java what to do when a method is intercepted. This is where the InvocationHandler comes in. Think of it as a traffic controller. Every time a method is called on your proxy object, Java bundles the proxy instance, the method being called, and the arguments into a single invoke method.

Here is how I would set up a simple performance monitor to replace those dozens of decorators Sarah was writing:

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

public class TimingHandler implements InvocationHandler {
    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        
        // This is where we actually call the real method on the real object
        Object result = method.invoke(target, args);
        
        long end = System.currentTimeMillis();
        System.out.println(method.getName() + " took " + (end - start) + "ms");
        
        return result;
    }
}

Notice that the invoke method is generic. It doesn't care if you're calling saveUser() or calculateTax(); it treats everything as a Method object and a set of arguments. This is the power of reflection—we're operating on the structure of the code rather than the hard-coded implementation.

Binding the Proxy to an Interface

Now that we have the handler, we need to actually instantiate the proxy. You use the Proxy.newProxyInstance method for this. This method requires three things: the class loader, an array of interfaces the proxy should implement, and your InvocationHandler.

One critical detail you need to remember: Dynamic Proxies in Java (using the JDK's built-in reflection) only work with interfaces. You cannot create a dynamic proxy for a concrete class that doesn't implement an interface. If you need to proxy a class, you'd have to look into libraries like CGLib or ByteBuddy, but for most architectural needs, interfaces are the way to go.

public interface UserService {
    void createUser(String name);
}

public class UserServiceImpl implements UserService {
    public void createUser(String name) {
        // Simulate some heavy database work
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        System.out.println("User " + name + " created.");
    }
}

// Putting it all together:
UserService realService = new UserServiceImpl();
UserService proxyService = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class>[] { UserService.class },
    new TimingHandler(realService)
);

proxyService.createUser("Alice"); 
// Output: 
// User Alice created.
// createUser took 105ms

I've seen developers over-use this pattern, which is a trap. It's tempting to put every single piece of logic into a proxy, but remember that reflection carries a small performance overhead and makes debugging harder since the stack trace will be littered with sun.reflect.GeneratedMethodAccessor calls. Use it for cross-cutting concerns—logging, security, transactions, and profiling—not for core business logic.




📋 Practical Task

Build a Method-Level Access Control Proxy

Your task is to create a security proxy that prevents certain methods from being called based on a mock "user role."

  • Create an interface called DocumentService with two methods: readDocument() and deleteDocument().
  • Create a concrete implementation DocumentServiceImpl that simply prints "Reading document..." and "Deleting document..." respectively.
  • Implement an AccessControlHandler (implementing InvocationHandler). This handler should have a String userRole field.
  • In the invoke method, add logic that:
    • Always allows readDocument() to execute.
    • Only allows deleteDocument() to execute if the userRole is equal to "ADMIN".
    • Throws a SecurityException if a "USER" attempts to call deleteDocument().
  • In your main method, instantiate the proxy twice: once with the "ADMIN" role and once with the "USER" role. Demonstrate that the Admin can delete, but the User cannot.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.