Java Dynamic Proxies: One Step from Aspect-oriented Programming

Java Dynamic Proxies: One Step from Aspect-oriented Programming

ynamic proxies are a nifty, often overlooked feature of the Java language. Introduced in J2SE 1.3, they allow you to intercept method calls so you can interpose additional behavior between a class caller and its “callee”. From the caller’s perspective, a proxy is no different from the real class that it encapsulates—it presents the same interfaces and follows all the rules and conventions expected of the real class.

For savvy programmers, proxies present a unique timesaving opportunity. Why spend effort inserting and maintaining such common code as debugging and logging when you can just centralize it and put in a proxy instead? Logging, retry semantics, performance metrics, performance optimizations, test stubs, and caching are examples of application concerns that cut across class implementations regardless of design and business function.

Trends within the Java community are modeling such concerns as aspects—important to the application but orthogonal to the main task at hand. Rather than coding this logic painstakingly throughout your application, wouldn’t it be nice if you could just express this behavior in one place and have it apply to every class? With dynamic proxies, you can easily do so—and once you understand proxies, you’ve made a small conceptual leap towards the bigger picture—aspect-oriented programming (AOP).

Dynamic Proxy Basics
A dynamic proxy is a class that implements a list of interfaces, which you specify at runtime when you create the proxy. To create a proxy, use the static method java.lang.reflect.Proxy::newProxyInstance(). This method takes three arguments:

  • The class loader to define the proxy class
  • An invocation handler to intercept and handle method calls
  • A list of interfaces that the proxy instance implements

The returned Proxy instance behaves like any other class that implements the supplied interfaces. The statement, proxy instanceof MyInterface, returns true and casts to any of the target interfaces. For example, MyInterface obj = (MyInterface) proxy will also succeed.

From the code perspective, the proxy instance is no different from a normal class. Provided the actual creation of the proxy is encapsulated (e.g., inside a factory or ServiceLocator), the proxy is totally transparent. Behavior can be inserted dynamically without changing the way your classes are wired or how your code interacts. This transparency enables you to add common code easily and quickly without affecting class collaborations or having to add (or test) your extra code explicitly for every class.

At the heart of the Proxy mechanism is the InvocationHandler interface. You must supply an InvocationHandler instance when you create a proxy instance. The InvocationHandler is responsible for intercepting and dispatching all method calls made on the proxy. It contains one method: invoke(Object proxy, Method method, Object[] args). The first argument is the proxy instance that has been invoked, the second argument is the target method, and the third argument (args) are the runtime parameters that the caller supplies. You must add any additional behavior inside the invoke() method, as this will get executed whenever a method is executed on the proxy.

Proxy by Example
Take a common crosscutting concern, logging, as an example. Create a simple InvocationHandler class that logs method entries and exits using System.out.println (download the sample source code). Your class must implement the InvocationHandler interface and provide an implementation of the invoke() method. Pass the real class in as a parameter on the constructor, as follows:

public class LoggingHandler implements InvocationHandler {	protected Object delegate;	public LoggingHandler(Object delegate) {		this.delegate = delegate;	}	public Object invoke(Object proxy, Method method, Object[] args)		throws Throwable {		try {			System.out.println(				"Calling method "					+ method					+ " at "					+ System.currentTimeMillis());			Object result = method.invoke(delegate, args);			return result;		} catch (InvocationTargetException e) {			throw e.getTargetException();		} finally {			System.out.println(				"Called method("					+ method					+ " at "					+ System.currentTimeMillis());		}	}}

Exception Handling
Your InvocationHandler should aim to mimic the real interfaces, even conforming to the interface exception strategy. If your handler adopts a different exception handling strategy from what is declared on the real interfaces, then your proxies will behave differently from your real classes. Your proxies will no longer be transparent and you may have to add special-purpose code to handle the differences.

In the LoggingHandler, if the underlying delegate method throws an exception, then the call to method.invoke() throws an InvocationTargetException exception. If the handler method throws a checked exception, then it must be assignable to one of the declared exceptions in the throws clause of the interface method being called. If not, the proxy will propagate a runtime exception—UndeclaredThrowableException—to the caller. Avoid this by adopting one or more of the following approaches:

  • Never throw checked exceptions that are not declared on the interface (although this can be difficult for general-purpose handlers).
  • Adopt a common application-level exception to be thrown by all interface methods. That way, you can assume that the exception is always thrown by your interface. Wrap any checked exceptions inside the common exception.
  • For reflection calls, ensure that the real exception is communicated to the caller by extracting the underlying exception using InvocationTargetException::getTargetException() and re-throwing. This way, all exceptions that are declared on the interface and thrown inside the real class will be propagated back to the caller.

To test the LoggingHandler, define an interface (Test) that contains one method [ping()] and a class (TestImpl) that implements Test:

public interface Test {		public void ping()throws Exception;}public class TestImpl implements Test {		public void ping()throws Exception {		System.out.println("ping()");	}}

Create an instance of TestImpl and LoggingHandler and pass TestImpl into the LoggingHandler constructor. Create a dynamic proxy and pass in the LoggingHandler:

// Create an instance of TestImplTest t = new TestImpl();// Create InvokeHandlerInvocationHandler handler = new LoggingHandler(t);Create a dynamic proxy and pass in the LoggingHandler.// Create ProxyTest proxy =	(Test) Proxy.newProxyInstance(	t.getClass().getClassLoader(),	t.getClass().getInterfaces(),	handler);

Execute the real class TestImpl and then execute the proxy. The output from the proxy should be identical to the output of TestImpl except for additional method diagnostics:

// Test real;ping()// Test Proxy;Calling method public abstract void throws java.lang.Exception at 1088912557093ping()Called method(public abstract void throws java.lang.Exception at 1088912557093

The LoggingHandler is flexible in that it can be added to any arbitrary class (via a proxy) without changing your class logic. The handler centralizes your logging code and, by keeping your logging semantics distinct from your business functions, reduces code clutter.

Remote Proxies
The simple Logging example in the previous section indicates how easily you can incorporate more sophisticated logic such as performance diagnostics, metric collection, and result caching into your code. However, that is just the start. For distributed applications, proxies open up more interesting possibilities.

One of the major concerns of distributed computing is the management of client/server communication. Typically, the responsibility for managing low-level communication falls to the client code. Business-level functions can get mixed in with low-level networking, error handling, and retry code, which increases coupling and makes your code messy and hard to maintain. One way to handle this is to centralize the networking aspects of your system inside remote proxies or business delegates (Marinescu). From the client’s perspective, the proxy appears just like a normal class. Any problems communicating with the backend are hidden, and network-related errors such as RemoteExceptions or EJBExceptions get translated into something more meaningful to the client.

Remote proxies are useful but extremely tedious to write if you have to create them for every single Remote or EJB interface. Dynamic proxies can alleviate the problem by providing a common, type-safe component for mediating client/server communication. The first step is to ensure that your code for locating remote services is centralized. This can be a simple factory class or more likely a ServiceLocator class. Locator classes hide the complexity of discovering and creating server-related artifacts. You can either add your dynamic proxy to your existing ServiceLocator or create a new Factory/Locator that wraps the old one. Here’s a simple ServiceLocator class that returns a proxy instead of an EJB instance:

public final class ServiceLocator {		/**	 * Locate a Remote service	 **/	public static Object locate(Class classInterface) throws NetworkException {		// Create Handler		InvocationHandler handler = new RemoteProxyHandler(classInterface);				// Create Remote Proxy		Object proxy = Proxy.newProxyInstance(classInterface.getClassLoader(),			new Class[] { classInterface}, handler);					return proxy;	}	}

When a method is executed on the proxy, it is the RemoteProxyHandler’s responsibility to manage the EJB specifics: EJBHome location, EJB creation, and remote method execution. This way, the client is totally shielded from the complications of using a remote service:

public class RemoteProxyHandler implements InvocationHandler {	public static final int RETRY_LIMIT = 3;	...	public Object invoke(Object proxy, Method method, Object[] args)		throws Throwable {			// find home			if (home == null) {				home = findHome(classInterface);			}			// create ejb			if (bean == null) {				bean = createEjb(home);			}			// invoke method			Object result = invokeRemoteMethod(method, args);			return result;			}

You can add common error handling and retry semantics to all remote calls transparently:

protected Object invokeRemoteMethod(Method method, Object[] args)		throws ApplicationException {				int trys = 1;		while (trys <= RETRY_LIMIT) {			try {				return method.invoke(bean, args);							} catch (IllegalAccessException ie) {				throw new ApplicationException(ie);			} catch (InvocationTargetException e) {				if (trys < RETRY_LIMIT) {                        ++trys;                        				} else {                        throw new ApplicationException(e.getTargetException());									} //fi			} //end catch		} //end while		return null;			}

Transaction Proxies
Transactions are another crosscutting concern you could consider. In J2EE, EJB Session beans manage your transactions. You make a method transactional by marking it as TX_NEW or TX_REQUIRED in the EJB descriptor. However, not all applications run inside an EJB container or have access to Session beans. In order to execute updates to a DBMS in a transaction manner, you must code the low-level connection management and transaction demarcation logic yourself.

Rather than explicitly creating transactions within your code, you can delegate this responsibility to a transaction proxy. The proxy will ensure that your methods are executed within a transactional context. If your real method completes, then any updates performed inside your method will be committed. Conversely if your method fails, all updates will be rolled back:

public class TransactionProxyHandler implements InvocationHandler {	protected Object delegate;	public TransactionProxyHandler(Object delegate) {		this.delegate = delegate;	}	public Object invoke(Object proxy, Method method, Object[] args)		throws Throwable {		// Get connection		Connection conn = null;		try {			// Get a connection 			// subsequent getConnection() calls will return the same connection			// from the DAOUtil (backed by threadlocal)			conn = DAOUtil.getConnection();						// start a transaction			conn.setAutoCommit(false);						// execute the method			Object result = method.invoke(delegate, args);						// commit any statements on the current connection			conn.commit();						return result;					} catch (InvocationTargetException e) {			// rollback transaction			conn.rollback();			throw e.getTargetException();		} finally {			//	Close the connection for this call in this thread			DAOUtil.closeConnection(conn);		}	}}

You can also aggregate or chain your proxies via your Invocation Handlers. This way, you can combine Logging with Transactions or Logging with Remote handling, etc. Instead of passing the real class as the delegate to the Invocation Handler constructor, pass another proxy instead. The order in which you construct your proxies will determine the order in which your behavior gets invoked:

 // Create an instance of TestImpl Test t = new TestImpl();              // Create Transaction Handler InvocationHandler txnHandler = new TransactionProxyHandler(t);        // Create Txn Proxy  Test txnProxy =  	(Test) Proxy.newProxyInstance(       t.getClass().getClassLoader(),       t.getClass().getInterfaces(),       txnHandler);                   // Create Log Handler and wrap around Transaction proxyInvocationHandler loggingHandler = new LoggingProxyHandler(txnProxy); // Create Proxy with 2 levels of indirectionTest proxy =      (Test) Proxy.newProxyInstance(             t.getClass().getClassLoader(),             t.getClass().getInterfaces(),             loggingHandler);                    // Test Proxy class;

Although useful, dynamic proxies are not always appropriate. Code simplification comes at the expense of performance. Each level of indirection adds overhead—this is especially true of reflection-based mechanisms. Take care to keep your proxies lightweight. Ensure that your handler code is simple, and be selective in which classes you target.

The Next Step: Aspect-oriented Programming
Now that you understand dynamic proxies, you may wonder whether you can implement more of your application logic in this way. You can—it’s called aspect-oriented programming (AOP). Dynamic proxies illustrate the basics of AOP. Proxies are just one way that aspect-oriented behavior or crosscutting concerns can be added dynamically to your code. AOP complements existing object-oriented design by facilitating the consolidation and modularization of common functions that would normally be interleaved throughout your code. The following are just some examples of AOP frameworks that enable aspects to be woven transparently into your code:


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist