Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Dynamic Proxy Logging for Method Calls

Recommended resources

Introduction

In our previous lesson, we explored the mechanics behind Java's dynamic proxies, discovering how java.lang.reflect.Proxy generates new classes at runtime to intercept method calls. We learned that the InvocationHandler is the central component where all proxied method calls are routed. Now, it's time to put that knowledge into practice.

This lesson focuses on a highly practical application: implementing a simple logging aspect. An "aspect" in programming refers to a feature that cuts across multiple parts of your application, like logging, security, or transaction management. By the end of this lesson, you will be able to build a dynamic proxy that automatically logs details about method calls—such as the method name, arguments, and execution time—without modifying the original business logic. This is a foundational skill for understanding how powerful frameworks like Spring AOP operate under the hood.


The Anatomy of a Logging Aspect

The goal is to wrap an object with a proxy that performs logging before and after every method call. The core of this functionality will reside within our custom InvocationHandler.

Here's the plan for our invoke method:

  1. Record the start time.
  2. Log a message indicating which method is being called, including its arguments.
  3. Use reflection to invoke the actual method on the original, "target" object.
  4. Wrap the invocation in a try...finally block to ensure our "after" logic runs even if the method throws an exception.
  5. In the finally block, record the end time and log the execution duration.
  6. Return the result from the original method call.

Let's start with a simple service we want to add logging to. Imagine a basic data-access interface and its implementation:

// The interface we want to proxy
public interface DataRepository {
    String fetchData(String id);
    void saveData(String id, String data);
}

// The real object with business logic
public class SimpleDataRepository implements DataRepository {
    @Override
    public String fetchData(String id) {
        // Simulate a database call
        System.out.println(">> Inside SimpleDataRepository.fetchData: fetching data for id " + id);
        try {
            Thread.sleep(100); // Simulate latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "Data for " + id;
    }

    @Override
    public void saveData(String id, String data) {
        // Simulate writing to a database
        System.out.println(">> Inside SimpleDataRepository.saveData: saving data for id " + id);
        try {
            Thread.sleep(50); // Simulate latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Notice the System.out.println calls within the methods. Our goal is to replace this kind of intrusive logging with an external, dynamic solution.


Implementing the LoggingInvocationHandler

Now, we'll create the InvocationHandler that contains our logging logic. This handler needs a reference to the real object so it can delegate the actual method call to it.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public class LoggingInvocationHandler implements InvocationHandler {

    private final Object target; // The real object we are proxying

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long startTime = System.nanoTime();

        System.out.printf("[PROXY] Calling method: %s(%s)%n",
                method.getName(), Arrays.toString(args));

        Object result = null;
        try {
            // Delegate the call to the real object
            result = method.invoke(target, args);
        } catch (Exception e) {
            System.out.printf("[PROXY] Exception in method %s: %s%n", method.getName(), e.getCause());
            throw e; // Re-throw the exception
        } finally {
            long endTime = System.nanoTime();
            long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
            System.out.printf("[PROXY] Finished method: %s, execution time: %d ms%n",
                    method.getName(), duration);
        }
        return result;
    }
}

Tying It All Together

With the interface, implementation, and handler ready, the final step is to use Proxy.newProxyInstance() to create our logging proxy and see it in action.

import java.lang.reflect.Proxy;

public class ProxyDemo {
    public static void main(String[] args) {
        // 1. Create the real object
        DataRepository realRepository = new SimpleDataRepository();

        // 2. Create the invocation handler, passing the real object to it
        InvocationHandler loggingHandler = new LoggingInvocationHandler(realRepository);

        // 3. Create the dynamic proxy instance
        DataRepository proxyRepository = (DataRepository) Proxy.newProxyInstance(
                DataRepository.class.getClassLoader(),
                new Class<?>[] { DataRepository.class },
                loggingHandler
        );

        // 4. Use the proxy object just like the real one
        System.out.println("--- Calling fetchData through proxy ---");
        String data = proxyRepository.fetchData("123");
        System.out.println("Result from proxy: " + data);

        System.out.println("\n--- Calling saveData through proxy ---");
        proxyRepository.saveData("456", "Some new data");

        System.out.println("\n--- Proxying is complete ---");
    }
}

When you run ProxyDemo, you will see output that clearly separates the proxy's logging from the actual business logic's output, demonstrating that our aspect is working correctly.

Expected Output:

--- Calling fetchData through proxy ---
[PROXY] Calling method: fetchData([123])
>> Inside SimpleDataRepository.fetchData: fetching data for id 123
[PROXY] Finished method: fetchData, execution time: 102 ms
Result from proxy: Data for 123

--- Calling saveData through proxy ---
[PROXY] Calling method: saveData([456, Some new data])
>> Inside SimpleDataRepository.saveData: saving data for id 456
[PROXY] Finished method: saveData, execution time: 51 ms

--- Proxying is complete ---

Real-World Examples and Framework Connections

What you've just built is the fundamental concept behind Aspect-Oriented Programming (AOP) in major Java frameworks. The logging, timing, or transaction logic is the "advice," and the dynamic proxy is the mechanism that "weaves" this advice around your code at runtime.

The official Java documentation provides a similar example.

Dynamic Proxy Classes

The official Oracle documentation includes a simple example of a DebugProxy that prints messages before and after method calls. This serves as a great validation of the pattern we've just implemented.

Scroll down to the "Examples" section. Study the DebugProxy implementation. You'll see it follows the same core pattern: an InvocationHandler that prints a message, invokes the real method, and then prints another message.

The Baeldung article you've seen before also contains a TimingDynamicInvocationHandler which is another slight variation on this theme, focusing purely on performance measurement.

This is precisely how frameworks like Spring handle concerns that apply to many methods.

This image shows console logs from a Spring application. The top half shows logging from a method intercepted by a JDK Dynamic Proxy, while the bottom half shows interception via a CGLib proxy. Both are implementing a logging aspect, just as we did.

To see this principle applied to a different, but equally important cross-cutting concern—transaction management—the following video is extremely insightful. It walks through the evolution from a clunky, manual proxy to a clean, dynamic proxy solution.

Spring Boot In Depth : 6 - How Spring handles Transactions & Security using AOP/Proxies

This video from SivaLabs provides an excellent demonstration of why dynamic proxies are so powerful for implementing cross-cutting concerns like transactions.

First, watch the demonstration of a manual proxy from this segment. The author creates a CustomerServiceProxy class by hand. Notice how much boilerplate code is required and how this approach is not scalable if you have many services. Next, see how this is improved with a JDK Dynamic Proxy from this part. He creates a generic TransactionJdkProxy using an InvocationHandler, just like our logging example. This single handler can now provide transactional behavior to any service that implements an interface. Crucially, pay attention to the common pitfall discussed later in the video regarding self-invocation, starting around this timestamp. When a method inside a proxied object calls another method on this, it bypasses the proxy. The call is made directly on the target object, so the proxy's logic (logging, transactions, etc.) is not triggered for the inner call. This is a critical nuance to understand when working with proxy-based AOP.


Conclusion

In this lesson, you've moved from theory to practice by implementing a functional logging aspect using Java's dynamic proxies. This hands-on experience solidifies your understanding of how to intercept and add behavior to method calls dynamically.

Key Takeaways:

  • Dynamic proxies are an ideal mechanism for implementing cross-cutting concerns like logging, security, and transactions.
  • The InvocationHandler is the centerpiece of the implementation, containing the "advice" or extra logic you want to apply.
  • The core pattern involves creating the handler with a reference to the target object and using method.invoke(target, args) to delegate the original call.
  • This proxy-based approach is the foundation of Aspect-Oriented Programming (AOP) in popular frameworks like Spring.
  • Understanding the limitations, such as the self-invocation issue, is crucial for correctly using proxy-based systems.

In our final lesson for this module, we will expand on these ideas. We will use reflection not just for interception, but for dynamic object creation and wiring to build a miniature dependency injection (DI) container, giving you a glimpse into the heart of an application framework.