Generically chain dynamic proxies

本文介绍如何使用Java动态代理创建可扩展的拦截器链,实现面向切面编程(AOP)。通过三种不同方法展示如何灵活地为业务对象添加额外行为,如日志记录、同步控制等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Generically chain dynamic proxies

Add AOP concepts to your application

 

Summary
Most developers already know how to decorate a business object and add additional behavior to it at runtime. The Gang of Four (GoF) Decorator pattern helps developers achieve this functionality. Under the reflection package, J2SE 1.3 introduced the dynamic proxy, which dynamically decorates business objects. Additionally, chaining dynamic proxies can add multiple behaviors to business objects dynamically at runtime. More specifically, these types of additional behaviors are addressed by aspect-oriented programming (AOP). This article is not intended as an in-depth discussion on AOP; rather, it focuses on generically chaining the dynamic proxies so that the developer can implement some AOP concepts in a framework-driven way. If a project is already using some existing AOP framework, then the developer does not need to worry about implementing a custom framework. But developers who, for whatever reason, do not use these frameworks in their projects can still get the advantage of chaining dynamic proxies in an effective way with little effort. ( 4,500 words; January 30, 2006)
By Srijeeb Roy


Programming with plain-old Java objects (POJOs) is rather popular these days. When we program with POJOs, we can apply object-oriented programming (OOP) pretty easily. But sometimes implementing cross-cutting aspects throughout an application using OOP proves difficult. For example, generally implementing logging or security in our POJO business objects throughout an application is difficult to achieve with OOP. Dynamic proxies, which were introduced in J2SE 1.3, offer an easier solution.

The idea behind dynamic proxies is to include dynamic behavior around an object, yet neither change the object's existing code nor its interface. The famous Gang of Four (GoF) Decorator pattern provides a way to decorate an object (change its behavior) without changing the object code and allows us to add cross-cutting aspects to our business objects. Many existing frameworks and tools use this pattern. But when implementing static decorating, the pattern imposes some problems, which I discuss later in the article.

Prior to the introduction of dynamic proxies, there was no direct way to decorate objects dynamically. So vendors came up with tools to generate code automatically and decorate objects. Though a code-generation tool can help us generate static decorators, it requires extra steps and also introduces overhead for maintaining the generated code. By using dynamic proxies, we can greatly reduce this auto-generated code (perhaps to zero).

To see how dynamic proxies work, let's take an example of the classic Decorator pattern as a method interceptor and see what dynamic proxies can offer in its place. If we use dynamic proxies as is, we may face some coding complexities. Later in the article, you will see how to wrap these complexities involved with dynamic proxies and provide an abstraction over them. Most of the source code used in this article can be downloaded from Resources.

Static decorating and chaining without dynamic proxies
Suppose we have a simple business interface:

public interface IMyBusinessObject {
   public String doExecute(String in);
}

 

And this implementation of a business object class:

public class MyBusinessObject implements IMyBusinessObject {
   public String doExecute(String in) {
      System.out.println("Here in MyBusinessObject doExecute: input :" + in);
      return in;
   }
}

 

Now we want to add some behavior (e.g., logging) before and after the doExecute() method. The Decorator pattern helps us easily add this functionality.

We define an abstract decorator class like the following:

public abstract class ADecorator implements IMyBusinessObject {
   protected IMyBusinessObject target;
   public void setTarget(IMyBusinessObject target_) {
      this.target = target_;
   }
   public ADecorator(){}
   public ADecorator(IMyBusinessObject target_) {
      setTarget(target_);
   }
}

 

Now we define a concrete DebugConcreteDecorator, which extends ADecorator. Our intention is to add debug messages before and after our business object's method invocation:

public class DebugConcreteDecorator extends ADecorator {
   public String doExecute(String in) {
      System.out.println("DebugConcreteDecorator: before method : doExecute ");

      String ret = target.doExecute(in);
      System.out.println("DebugConcreteDecorator: after method : doExecute ");
      return ret;
   }
}

 

Now from the client code, we call our business object:

IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
IMyBusinessObject wrappedObject =
   new DebugConcreteDecorator(aIMyBusinessObject);
wrappedObject.doExecute("Hello World");

 

In the above code snippet, we wrap our business object with the DebugConcreteDecorator class instance. Since DebugConcreteDecorator extends ADecorator and ADecorator implements the business object interface IMyBusinessObject, the DebugConcreteDecorator class is itself an instance of IMyBusinessObject. First, we create an instance of MyBusinessObject. Next, we create an instance of DebugConcreteDecorator and pass the business object in the constructor. Since no constructor is in DebugConcreteDecorator, the constructor of its superclass (ADecorator) is called and the target object is set with the MyBusinessObject instance. So when the doExecute() method is called on the DebugConcreteDecorator instance, it first logs some debug message (using System.out) and then calls the business method on the actual business object (MyBusinessObject).

The above invocation's output is the following:

DebugConcreteDecorator: before method : doExecute
Here in MyBusinessObject doExecute: input :Hello World
DebugConcreteDecorator: after method : doExecute

From this output, we observe that we are able to add the debug messages before and after the business method invocation.

We can also chain the decorators—call one decorator after another decorator—before invoking actual business methods. Let's define another decorator to show this approach:

public class AnotherConcreteDecorator extends ADecorator {
   public String doExecute(String in) {
      System.out.println("AnotherConcreteDecorator: Going to execute method : doExecute");
      in = in + " Modified by AnotherConcreteDecorator";
      String ret = target.doExecute(in);
      System.out.println("AnotherConcreteDecorator: After execute method : doExecute");
      return ret;
   }
}

 

The above code snippet modifies the business method's input string parameter by adding an extra string (" Modified by AnotherConcreteDecorator") to it.

If we want to chain the decorators, we write the following code snippet in the client:

IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
IMyBusinessObject wrappedObject =
   new AnotherConcreteDecorator (new DebugConcreteDecorator(aIMyBusinessObject));
wrappedObject.doExecute("Hello World");

 

In the above code snippet, we create a DebugConcreteDecorator instance by passing the actual target business object instance into it. Then we wrap the DebugConcreteDecorator instance with our new AnotherConcreteDecorator instance. So when the doExecute() method is called in the AnotherConcreteDecorator instance, AnotherConcreteDecorator first modifies the input parameter by adding the extra string. Then the call forwards to the DebugConcreteDecorator instance's doExecute() method. There, DebugConcreteDecorator logs the entry of the doExecute() method and calls the doExecute() method of the actual business object's doExecute() method.

The return path is the exact opposite sequence. After returning from the actual business object's (MyBusinessObject) doExecute() method, the rest of the code in DebugConcreteDecorator executes. Then the call returns to the AnotherConcreteDecorator instance and executes the rest of its part.

The output of the above invocation is the following:

AnotherConcreteDecorator: Going to execute method : doExecute
DebugConcreteDecorator: before method : doExecute
Here in MyBusinessObject doExecute: input :Hello World Modified by AnotherConcreteDecorator
DebugConcreteDecorator: after method : doExecute
AnotherConcreteDecorator: After execute method : doExecute

The class diagram of the above approach appears in Figure 1.

 

 


Figure 1. Class diagram of decorating the business object with static decorator

 

 

Now let's look into the issues with static decorating.

Look inside the doExecute() method of either of the two decorators (DebugConcreteDecorator or AnotherConcreteDecorator). It makes a hard-coded call to the target object's doExecute() method. Also, if we define another method in the IMyBusinessObject interface, we must override that in all the concrete decorators and provide an implementation for the method. So in practicality, we may end up with many decorators and a lot of code inside each decorator. A dynamic proxy helps us remove those hard-coded calls. In addition, we don't need to override and implement each method of the business interface inside the decorators.

I do not go into detail about dynamic proxies in this article. Instead, I take a small example and show how a dynamic proxy works. Then we jump into the chaining of dynamic proxies to intercept method calls in a generic way.

J2SE 1.3 dynamic proxies: An example
A dynamic proxy class is a class that implements a list of interfaces specified at runtime when the class is created. A proxy interface is an interface implemented by a proxy class and an instance of the java.lang.reflect.Proxy class. Each proxy instance has an associated invocation handler object, which implements the interface java.lang.reflect.InvocationHandler. A method invocation on a proxy instance through one of its proxy interfaces is dispatched to the invoke method of the instance's invocation handler, passing the proxy instance, a java.lang.reflect.Method object identifying the method that was invoked, and an array of type java.lang.Object that contains the arguments. The invocation handler processes the encoded method invocation as appropriate and the result it returns is returned as the result of the method invocation on the proxy instance.

For example, say we have the same interface IMyBusinessObject and the business class MyBusinessObject used in the earlier example. Now while using the dynamic proxy, we must write an invocation handler since the java.lang.reflect.Proxy class will use this handler.

The DebugInvocationHandler class looks like this:

public class MyDebugInvocationHandler
   implements java.lang.reflect.InvocationHandler  {

   private Object target = null;
   public void setTarget(Object target_) {
      this.target = target_;
   }
   public Object invoke(Object proxy, Method method,
      Object[] args) throws Throwable {
      try {
         System.out.println("Going to execute method : " + method.getName);
         Object retObject = method.invoke(target, args);
         System.out.println("After execute method : " + method.getName());
         return retObject;
      } catch(InvocationTargetException e) {
         throw e.getTargetException();
      } catch(Exception e) {
         throw e;
      }
   }
}

 

In the above example, the invoke() method is important and is invoked through the java.lang.reflect.Proxy class. Inside this method, we can do our extra processing and then forward this processing to the real target object (in our case, an instance of MyBusinessObject).

So our client side code can be the following:

IMyBusinessObject bo = new MyBusinessObject();
MyDebugInvocationHandler aMyDebugInvocationHandler =
   new MyDebugInvocationHandler();
aMyDebugInvocationHandler.setTarget(bo);

IMyBusinessObject proxyObject =
   (IMyBusinessObject) Proxy.newProxyInstance
      (IMyBusinessObject.class.getClassLoader(),
      new Class[] { IMyBusinessObject.class },
      aMyDebugInvocationHandler);

System.out.println(proxyObject.doExecute("Hello World"));

In the code above, we create an instance of MyBusinessObject and an instance of MyDebugInvocationHandler. We set the target as MyBusinessObject in MyDebugInvocationHandler such that when invoke() is called, it can pass the request to the proper target. Then we create a proxy object of the IMyBusinessObject interface using java.lang.reflect.Proxy. After that, we invoke a method on the proxy. It is important to note that since the invoke() method deals with the generic java.lang.reflect.Method class and no business-interface-specific methods, the need to write separate invocation handlers for separate business interfaces is not required. Also, if we want to implement some cross-cutting aspect through all the business methods, we do not have to implement all the business methods defined in the business interface. For example, to implement security in all our business methods, we must implement the security logic in only one place and within one method (SecurityInvocationHandler's invoke() method, which we will write in a generic way).

If we want to add multiple handlers in the chain, we must create another invocation handler. Then in the new handler's setTarget() method, instead of setting the instance of MyBusinessObject, we set the previous proxy object of the chain. Thus the code would appear as follows:

MyAnotherInvocationHandler aMyAnotherInvocationHandler = new MyAnotherInvocationHandler ();

//Here we will set the proxyObject, which we get through earlier
//code snippet, instead of the business object instance
aMyAnotherInvocationHandler.setTarget(proxyObject);

IMyBusinessObject nextProxyObject =
   (IMyBusinessObject) Proxy.newProxyInstance
      (IMyBusinessObject.class.getClassLoader(),
      new Class[] { IMyBusinessObject.class },
      aMyAnotherInvocationHandler);

System.out.println(nextProxyObject.doExecute("Hello World"));

 

From the above example, we can see how using a dynamic proxy adds the supplementary behavior dynamically, with less code than static decorator chaining. However, if we use the dynamic proxy as shown above, some problems remain: You still must write a lot of code while creating and chaining the dynamic proxies. And you have to deal with the proxy interface API, which is not as user-friendly as normal object creation or object creation from a factory class. Also, when we need to proxy our business classes, repeating the same code in multiple places is not a good idea.

The rest of this article tries to solve these problems. We will write a generic proxy factory that can expose simple APIs (hiding the proxy object creation and chaining within it), but can still provide the flexibility of the dynamic proxy.

Generically chain a dynamic proxy: Approach 1
Wouldn't it be nice if our client could call the business object using the following piece of code?

String[] interceptorClasses = {"MyDebugInterceptor",
                              "MyAnotherInterceptor"};
IMyBusinessObject aIMyBusinessObject =
   (IMyBusinessObject)MyProxyFactory.getProxyObject

      ("MyBusinessObject", interceptorClasses);
String ret = aIMyBusinessObject.doExecute("Hello World");  

The intention in the code above is to provide the class names of the interceptors as a java.lang.String array and to provide the business class name inside a method of the MyProxyFactory class. We expect that MyProxyFactory will create the business object, wrap it using the interceptors passed within the getProxyObject() method as the second parameter, and return us the wrapped proxy object. Now if we invoke the doExecute() business method, all the interceptors should execute in a chain and the actual method should be finally invoked.

It would also be nice if MyDebugInterceptor and MyAnotherInterceptor were generic and didn't vary with our business interface (IMyBusinessObject). To achieve this functionality, we now examine the following steps.

For this approach, we assume method interception will be done independently, before and after business method execution.

Let's define an interface for our interceptors:

public interface IMethodInterceptor {
   Object interceptBefore(Object proxy, Method method,
      Object[] args, Object realtarget);
   void interceptAfter(Object proxy, Method method,
      Object[] args, Object realtarget, Object retObject,
      Object interceptBeforeReturnObject);
}

 

Our intention is to call the interceptBefore() method before executing any of our business methods and the interceptAfter() method after successfully executing our business methods (without throwing any exceptions).

Now we write two implementations of IMethodInterceptor:

public class MyDebugInterceptor implements IMethodInterceptor {

   public Object interceptBefore(Object proxy, Method method,
      Object[] args, Object realtarget) {

      System.out.println("MyDebugInterceptor: Going to execute method : ");
      return null;
   }
   public void interceptAfter(Object proxy, Method method, Object[] args,
      Object realtarget, Object retObject, Object interceptBefore) {

      System.out.println("MyDebugInterceptor: After execute method : " );
   }
}

 

In the first implementation above, MyDebugInterceptor's interceptBefore() method intercepts the request before the method dispatches to the target object. interceptBefore() just prints a line to the console.

public class MyAnotherInterceptor implements IMethodInterceptor {


   public Object interceptBefore(Object proxy, Method method,
      Object[] args, Object realtarget) {

      System.out.println("MyAnotherInterceptor: Going to execute method : ");
      if ( method.getName().equals("doExecute") &&
         args != null && args.length >= 1 ) {

         if ( args[0] instanceof String ) {
            args[0] = args[0] +
               " Modified by MyAnotherInterceptor";
         }
       return null;
      }
   }

   public void interceptAfter(Object proxy, Method method, Object[] args,
      Object realtarget, Object retObject, Object interceptBefore) {

      System.out.println("MyAnotherInterceptor: After execute method : ");
   }
}

 

In this second implementation, MyAnotherInterceptor's interceptBefore() method also intercepts the request before the method dispatches to the target object. This time, it modifies the request parameter and changes its value (if the method name is doExecute()). It appends the extra string "Modified by MyAnotherInterceptor" in the existing input string parameter.

The next step is to write a generic invocation handler that can deal with the interceptors we have just written:

public class GenericInvocationHandler
      implements java.lang.reflect.InvocationHandler {

   private Object target = null;
   public void setTarget(Object target_) {
      this.target = target_;
   }
   private Object realtarget = null;
   public void setRealTarget(Object realtarget_) {
      this.realtarget = realtarget_;
   }
   IMethodInterceptor methodInterceptor = null;
   public void setMethodInterceptor
      (IMethodInterceptor methodInterceptor_) {
      this.methodInterceptor = methodInterceptor_;
   }
   public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
      try {
         Object interceptBeforeReturnObject = null;
         if ( methodInterceptor != null ) {
            interceptBeforeReturnObject =
               methodInterceptor.interceptBefore
                 (proxy, method, args, realtarget );
         }
         Object retObject = method.invoke(target, args);
         if ( methodInterceptor != null ) {
            methodInterceptor.interceptAfter
               (proxy, method, args, realtarget,
               retObject, interceptBeforeReturnObject );
         }
         return retObject;
      }
      catch(InvocationTargetException e) {
         throw e.getTargetException();
      }

      catch(Exception e) {
         throw e;
      }
   }
}

 

Look how interceptBefore() and interceptAfter() on the IMethodInterceptor interface have been plugged before and after the method invocation, respectively.

The next step is to write the MyProxyFactory class, which creates the proxies and chains them generically. This particular step should give you an idea of how to design your own framework using dynamic proxies:

public class MyProxyFactory {

   public static Object getProxyObject( String className,
      String[] interceptors ) throws Throwable {

      Object inputObject = getTargetObject(className);
      if ( interceptors != null && interceptors.length > 0  ) {

         Object inputProxiedObject = inputObject;
         for ( int i=0; i < interceptors.length; i++ ) {
            inputProxiedObject =
               getProxyObject(inputObject, interceptors[i],
                  inputProxiedObject);
         }
         return inputProxiedObject;
      }
      else {
         return inputObject;
      }
   }

   private static Object getProxyObject(Object inObject,
      String interceptor,Object inProxiedObject) throws Throwable {

      GenericInvocationHandler invocationHandler =
         new GenericInvocationHandler();
      IMethodInterceptor interceptorObject =
         (IMethodInterceptor)getInterceptor(interceptor);
      if ( interceptor == null ) {
         return inProxiedObject;
      }
      invocationHandler.setTarget(inProxiedObject);
      invocationHandler.setRealTarget(inObject);
      invocationHandler.setMethodInterceptor(interceptorObject);

      return Proxy.newProxyInstance
               (inObject.getClass().getClassLoader(),
               inObject.getClass().getInterfaces(),
               invocationHandler) ;
   }

   private static Object getInterceptor( String interceptors )
      throws Exception {    
      //...
      //From the class name return the class instance.
        //You can use Class.forName and newInstance() method on Class
        //to return the Object instance.
   }
   private static Object getTargetObject( String className )  
      throws Exception {
      //...
      //From the class name return the class instance.
        //You can use Class.forName and newInstance() method on Class
        //to return the Object instance.
   }
}

 

In this code's public static Object getProxyObject(String className, String[] interceptors) method, the iteration that goes through all the interceptors calls each interceptor's private static getProxyObject method, which creates an instance of GenericInvocationHandler every time. The method sets the target object and the interceptor on the instance of GenericInvocationHandler and passes the instance in the Proxy class's static newProxyInstance() method.

When the client code invokes the public static getProxyObject method, for the first iteration, it calls the private getProxyObject() with the following three parameters:

 

  1. The instance of the business object
  2. First interceptor name as a java.lang.String
  3. The same instance of the business object passed in the first parameter

A new proxy object wraps the instance of the business object with the first interceptor in the interceptor chain, and the proxy object is returned.

In the second iteration, the public static getProxyObject method then calls private getProxyObject with the following three parameters:

 

  1. The business object instance
  2. Second interceptor name as a java.lang.String
  3. The proxy object that wrapped the business object instance with the first interceptor

A new proxy object wraps the first proxy object (returned from the loop's first iteration) with the second interceptor in the interceptor chain, and the newly created proxy object is returned.

You should now get the idea: the chaining occurs generically inside the MyProxyFactory class. It is also important to note that, as the number of interceptors increases, the number of GenericInvocationHandlers and proxy instances also increase (as these have been created within a loop).

In the example, the class name is passed as java.lang.String, and Class.forName is used; in addition, the newInstance() method is used to instantiate the target class and also the interceptors. In a real implementation, using Class.forName and the newInstance() method every time can be problematic. We may want to set some variables in our business class or interceptors before invoking. Or we may want some of our business classes and interceptors to be defined as singleton instances. Those finer details must be handled in an actual implementation. In Approach 3, you will see how these particulars can be achieved to some extent.

The output of Approach 1's program matches the output of our static decorator. But here we have greater flexibility. Since MyProxyFactory and GenericInvocationHandler are generic, we can use these to wrap any business object. Of course, our business object must implement some predefined interface, which is a good programming practice.

The MyDebugInterceptor and MyAnotherInterceptor can also be generic and don't need to be separately written for each business interface and its methods. Suppose we want to do a security check before executing a business method; we can do it in an interceptor, within the interceptBefore() method. The security interceptor can read a configuration file/database and afterwards perform the security check. Let's say we have a custom security XML file like the following:

<security>
   <businessClass>
      <classname>MyBusinessObject<classname>
      <methodPermission>

         <method>doExecute</method>
         <validRoles>admin,manager</validRoles>
      </methodPermission>
   </businessClass>
</security>

 

This configuration can be read and cached. Later the security interceptor can do the processing accordingly by checking the name of the method and matching it with the role. (Note: You may be wondering how we can get the user's role in the interceptor, as we are not passing the role. That can be handled using ThreadLocal, but that is a separate topic.)

So inside our security interceptor's interceptBefore() method, the code resembles the following snippet:

public Object interceptBefore(Object proxy, Method method,
      Object[] args, Object realtarget) {
      
   String nameOfMethod = method.getName();
   String targetClassName = realtarget.getClass().getName();
   MethodPermissions mPerm =
      SecurityFactory.getPermission(targetClassName);
   If ( !mPerm.isAuthorized(MyThreadLocalRoleStore.getRole(),
                           nameOfMethod ) ) {
      throw new RuntimeException("User not authorized");
   }
   return null;
}

 

We still have to write MethodPermissions, SecurityFactory, MyThreadLocalRoleStore, etc., to implement the overall flow, but that extends beyond this discussion's scope. Also, the XML shown is for example purposes only; for a real implementation, more robust code (e.g., robust exception handling) is required. What happens if we have overloaded methods in our business object? The XML as well as the code inside interceptBefore() needs to be changed and other classes like MethodPermissions and SecurityFactory need to deal with those changes in detail.

Approach 1's class diagram appears in Figure 2.

 

 


Figure 2. Chain dynamic proxies: Approach 1 class diagram

 

 

Note: For simplicity and size constraints, methods and method parameters of some of the classes and interfaces in Figures 2 through 4 are not shown in detail.

In the above class diagram, note that the business interface (IMyBusinessObject) is not any way related with any other class or interface except the business object itself.

Dynamically chain a dynamic proxy: Approach 2
Approach 2 deals with the same set of classes as Approach 1, and from a client-call perspective, is also the same. The change proposed is in GenericInvocationHandler and in MyProxyFactory. As pointed out earlier in Approach 1, as the number of interceptors increases, the instances of GenericInvocationHandler and proxy instances also increase. Approach 2 tries to resolve this problem and achieve nearly the same benefit as Approach 1.

So the client code is the same as that in Approach 1:

String[] interceptorClasses = {"MyDebugInterceptor", "MyAnotherInterceptor"};
IMyBusinessObject aIMyBusinessObject =
   (IMyBusinessObject)MyProxyFactory.getProxyObject
      ("MyBusinessObject", interceptorClasses);
String ret = aIMyBusinessObject.doExecute("Hello World");

 

Our intention in this approach is to use only one instance of GenericInvocationHandler and proxy. Let's see our new GenericInvocationHandler code:

public class GenericInvocationHandler
      implements java.lang.reflect.InvocationHandler {

   private Object realtarget = null;
   public void setRealTarget(Object realtarget_) {
      this.realtarget = realtarget_;
   }
   Object[] methodInterceptors = null;
   public void setMethodInterceptors
      (Object[] methodInterceptors_) {
      this.methodInterceptors = methodInterceptors_;
   }
   public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable {
      try {
         Object[] retInterceptBefore = null;
         if ( methodInterceptors != null &&
            methodInterceptors.length > 0 ) {

            retInterceptBefore = new Object[methodInterceptors.length];
            for ( int i= methodInterceptors.length - 1; i >= 0; i-- ) {
               if ( methodInterceptors[i] != null ) {
                  retInterceptBefore[i] =
                     ((IMethodInterceptor)methodInterceptors[i]).
                        interceptBefore(proxy,
                           method,args, realtarget );
               }
            }
         }
         Object retObject = method.invoke(realtarget, args);
         if ( methodInterceptors != null ) {
            for ( int i= 0; i < methodInterceptors.length; i++ ) {
               if ( methodInterceptors[i] != null ) {
             ((IMethodInterceptor)methodInterceptors[i]).
                interceptAfter(proxy, method, args, realtarget,
                   retObject, retInterceptBefore[i] );
               }

            }
         }
         return retObject;
      }
      catch(InvocationTargetException e) {
         throw e.getTargetException();
      }
      catch(Exception e) {
         throw e;
      }
   }
}

 

Our GenericInvocationHandler takes the array of method interceptors (instead of a single interceptor like Approach 1) and before calling the method on the business interface, it loops through the interceptor array and calls each interceptor's interceptBefore() method. Similarly, after the method call, GenericInvocationHandler calls the interceptAfter() method in a loop. Please note that the loop goes in a reverse direction for interceptBefore() and forward for interceptAfter().

The code for MyProxyFactory also changes, but the getInterceptor() and getTargetObject() methods remain the same. The following code snippet comes from our new MyProxyFactory:

public static Object getProxyObject( String className,
   String[] interceptors ) throws Throwable {


   Object inputObject = getTargetObject(className);
   if ( interceptors != null && interceptors.length > 0  ) {
      return getProxyObject(inputObject, interceptors);
   }
   else {
      return inputObject;
   }
}
private static Object getProxyObject(Object inObject,
   String[] interceptors) throws Throwable {

   GenericInvocationHandler invocationHandler =
      new GenericInvocationHandler();
   Object[] interceptorObjects = getInterceptors(interceptors);
   invocationHandler.setRealTarget(inObject);
   invocationHandler.setMethodInterceptors(interceptorObjects);

   return Proxy.newProxyInstance
               (inObject.getClass().getClassLoader(),
               inObject.getClass().getInterfaces(),
               invocationHandler) ;
}
private static Object[] getInterceptors(String[] interceptors)
   throws Exception {

   Object[] objInterceptors = new Object[interceptors.length];
   for ( int i=0; i < interceptors.length; i++ ) {
      objInterceptors[i] = getInterceptor(interceptors[i]);
   }
   return objInterceptors;
}

 

From the above code snippet, we can see that there is only one instance of GenericInvocationHandler. In addition, only one instance of the proxy object is created and does not vary with the number of interceptors. Thus, the memory used is less; plus, Approach 2 is slightly faster than Approach 1. Later, I will compare results of all the approaches described with a simple test.

Some might point out that in Approach 2 we just loop through all the interceptors in GenericInvocationHandler instead of MyProxyFactory. The important point to note: In Approaches 1 and 2, the number of created interceptor instances is the same. In Approach 1, additional GenericInvocationHandlers and proxy objects are also created inside the loop. But in Approach 2, only one instance of GenericInvocationHandler and the proxy object is created.

The class diagram of Approach 2 is shown in Figure 3.

 

 


Figure 3. Generically chain dynamic proxies: Approach 2 class diagram

 

 

Approach 1 and Approach 2 can be made more useful by adding in the IMethodInterceptor interface a method (e.g., interceptException()) that can intercept any exception raised by the target (GenericInvocationHandler would also require modification). In addition, we can make different interfaces for all the three methods (interceptBefore(), interceptAfter(), and interceptException()). I advise you to break the interface into different smaller interfaces. For example, if we want to intercept only before executing the method, then only the interface containing the method interceptBefore() would be used and we need not worry about the implementation of the other methods. In aspect-oriented programming (AOP), intercepting in different parts of a program unit represents advice. So a separate interface can be created for before advice (compare to the interceptBefore() method), after return advice (compare to interceptAfter()), or throws advice (compare to interceptException()). Another type of advice, which is more general and flexible, can do all types of method interceptions together. But since it is a general approach, the developer's responsibility also increases (developer needs to forward the method call to next target). This advice type is called around advice. Approach 3 (discussed later) can be compared with AOP's around advice.

Approaches 1 and 2 both have limitations and cannot leverage all the advantages of the dynamic proxy without major modification. For example, since the interceptBefore() and interceptAfter() methods are called discretely, spanning some control between interceptBefore() and interceptAfter() would prove difficult. Let's say we want to write an interceptor that will make the target business object synchronized before invoking it. We want to achieve the following:

synchronized(realtarget) {

   retObject = method.invoke(target, args);
}

 

In Approach 3, we will learn how.

Generically chain a dynamic proxy: Approach 3
In Approach 3, the client code resembles that of Approaches 1 and 2:

String[] invocationHandlers = {"MyDebugInvocationHandler",
   "MyAnotherInvocationHandler"};
IMyBusinessObject aIMyBusinessObject =
   (IMyBusinessObject)MyProxyFactory.getProxyObject
      ("MyBusinessObject", invocationHandlers);
String ret = aIMyBusinessObject.doExecute("Hello World");

 

In this approach, we define the IMyInvocationHandler interface as follows:

public interface IMyInvocationHandler {
   void setTarget(Object target_);
   void setRealTarget(Object realtarget_);
}

 

We also define an abstract class that implements the IMyInvocationHandler and java.lang.reflect.InvocationHandler interfaces:

public abstract class AMyInvocationHandler
   implements IMyInvocationHandler,
      java.lang.reflect.InvocationHandler {

   protected Object target = null;
   protected Object realtarget = null;

   public void setTarget(Object target_) {
      this.target = target_;
   }
   public void setRealTarget(Object realtarget_) {
      this.realtarget = realtarget_;
   }
}

 

Approach 3 includes no GenericInvocationHandler class or IMethodInterceptor interface. Instead, our interceptors extend the abstract AMyInvocationHandler class. Let's look into our two interceptors. They now provide an implementation of the invoke() method defined by the java.lang.reflect.InvocationHandler interface:

 

  1. public class MyDebugInvocationHandler extends AMyInvocationHandler {

       public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {

          try {
             System.out.println("MyDebugInterceptor: Before execute method : "
                + method.getName());  
             Object retObject = method.invoke(target, args);
             System.out.println("MyDebugInterceptor: After execute method : "
                + method.getName());
             return retObject;
          }
          catch(InvocationTargetException e) {
             throw e.getTargetException();
          }
          catch(Exception e) {
             throw e;
          }
       }
    }

     

     

     

  2. public class MyAnotherInvocationHandler extends AMyInvocationHandler {

       public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {

          try {
             System.out.println("MyAnotherInvocationHandler: Before execute method : "
                + method.getName());        
             if ( method.getName().equals("doExecute")
                && args != null && args.length >= 1 ) {
                if ( args[0] instanceof String ) {
                   args[0] = args[0] + " Modified by MyAnotherInvocationHandler";
                }
             }
             Object retObject = method.invoke(target, args);
             System.out.println("MyAnotherInvocationHandler: After execute method : "
                + method.getName());
             return retObject;
          }
          catch(InvocationTargetException e) {
             throw e.getTargetException();
          }
          catch(Exception e) {
             throw e;
          }
       }
    }

     

     

In the MyDebugInvocationHandler and MyAnotherInvocationHandler classes, it is important to note that no distinct interface or interface methods are available for intercepting before or after doExecute(), or for throwing any exceptions with respect to the method invocation. Consider the Servlet's Filter interface, where we have only a doFilter() method; there are no doBefore() or doAfter() methods in the Filter interface. Approach 3 has similar functionality.

Approach 3 offers the developer more flexibility than the other approaches. For example, using Approach 3, let's see how we can achieve synchronization. We just have to implement a synchronization handler that will extend AMyInvocationHandler and perform the synchronization logic inside the invoke() method. To test the behavior, we need to ensure that multiple threads are accessing the doExecute() method simultaneously on the same business object instance. The sample synchronization handler code is provided below:

public class MySynchronizeInvocationHandler extends AMyInvocationHandler {

   public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
      
      Object retObject = null;
      synchronized(realtarget) {
         retObject = method.invoke(target, args);
      }
      return retObject;
   }
}

 

Now let's look into Approach 3's MyProxyFactory code snippet:

public static Object getProxyObject( String className,
   String[] invocationHandlers ) throws Throwable {

   Object inputObject = getTargetObject(className);
   if ( invocationHandlers != null &&
      invocationHandlers.length > 0  ) {

      Object inputProxiedObject = inputObject;
      for ( int i=0; i < invocationHandlers.length; i++ ) {
         AMyInvocationHandler myInvocationHandler =
            (AMyInvocationHandler)getInvocationHandler
               (invocationHandlers[i]);
         inputProxiedObject = getProxyObject(inputObject,
            myInvocationHandler, inputProxiedObject);
      }
      return inputProxiedObject;
   }
   else {
      return inputObject;

   }
}
public static Object getProxyObject( Object inputObject,
   Object[] invocationHandlers ) throws Throwable {

   if ( invocationHandlers != null
      && invocationHandlers.length > 0  ) {

      Object inputProxiedObject = inputObject;
      for ( int i=0; i < invocationHandlers.length; i++ ) {
         inputProxiedObject = getProxyObject(inputObject,
            (AMyInvocationHandler)invocationHandlers[i],
               inputProxiedObject);
      }

      return inputProxiedObject;
   }
   else {
      return inputObject;
   }
}
private static Object getProxyObject(Object inObject,
   AMyInvocationHandler myInvocationHandler,
      Object inProxiedObject) throws Throwable {

   if ( myInvocationHandler == null ) {
      return inProxiedObject;
   }
   myInvocationHandler.setTarget(inProxiedObject);
   myInvocationHandler.setRealTarget(inObject);

   return Proxy.newProxyInstance
                  (inObject.getClass().getClassLoader(),
                  inObject.getClass().getInterfaces(),
                  myInvocationHandler) ;
}

 

The code snippet is nearly the same as Approach 1's MyProxyFactory. This class also loops through the array of invocation handlers and creates the instance of each invocation handler and the proxy object. In Approach 1, the instances of GenericInvocationHandler, the interceptors, and the proxy objects are created inside a loop. In Approach 3, only the invocation handlers and the proxy object are created inside a loop (as there is no separate interceptor).

In the above code snippet, I also introduced another public, static method in the MyProxyFactory class, defined below:

public static Object getProxyObject
   ( Object inputObject, Object[] invocationHandlers )

 

Previously I discussed using Class.forName and some of its implications. We cannot set any variable earlier to the target object/invocation handlers/interceptors using the Class.forName approach. Thus, for simplicity, I provided the method above (you can also use the same method in Approaches 1 and 2). Now you can pass an existing instance of the target object or invocation handlers/interceptors. For example, the code on the client side may look like the following:

MyDebugInvocationHandler dHandler = new MyDebugInvocationHandler();
MyAnotherInvocationHandler aHandler = new MyAnotherInvocationHandler();
IMyBusinessObject bo = new MyBusinessObject();

//Now set any variables to these objects
//or some of these objects may be singletons


Object[] invocationHandlers = { dHandler, aHandler };
   (IMyBusinessObject)MyProxyFactory.getProxyObject
      (bo , invocationHandlers);
String ret = aIMyBusinessObject.doExecute("Hello World");

 

The class diagram of Approach 3 appears in Figure 4.

 

 


Figure 4. Generically chain dynamic proxies: Approach 3 class diagram

 

 

In addition to the approaches outlined in this article, several other variations exist. Specifically, the proxy factory can be written differently. Also, you can write the proxy factory such that it can mix and match Approaches 1 and 3.

A simple comparison among different approaches
I conducted a simple test to find out how much time each approach required for calling the actual business method after wrapping it with the interceptor. This test cannot be considered a scalability test since the machine configuration (like processor speed, number of processors, and available and used memory) was not considered. Still, from this simple test, we can get a rough idea about the performance of each approach.

To conduct the test, first, all the System.out.println statements were commented out (to ensure that I/O time wouldn't be a factor). Next, in each approach, the call to the business method using the proxy factory was called inside a loop that ran 1,000 times. On top of that, the same program was run 20 times. The following information shows the times in milliseconds for an average of 1,000 invocations (serially).

 

  • Static decorator chaining: 51.7 milliseconds for 1,000 invocations
  • Dynamic proxy chaining: Approach 1: 166.5 milliseconds for 1,000 invocations
  • Dynamic proxy chaining: Approach 2: 125.1 milliseconds for 1,000 invocations
  • Dynamic proxy chaining: Approach 3: 159.25 milliseconds for 1,000 invocations

Though the static decorator chaining requires the least amount of time, if we consider a single method invocation, the comparison comes down to a microsecond level, and that is not too much. And in practice, to gain some microseconds or milliseconds by rejecting a generic approach often results in a scattered approach with lots of custom coding, which essentially takes more execution time.

As mentioned earlier, we can mix and match Approaches 1 and 3 to build a good AOP framework. On the other hand, if we need specific cross-cutting aspects (all advantages cannot be achieved) to weave into our application, and we need to also consider performance in microseconds, then we can think about using Approach 2.

Conclusion
In this article's examples, the methods are static inside the factories. This approach can create future problems if you want to extend a factory or have multiple factory instances with different configurations. Hence, use the proper Factory pattern to write your factory.

I have only touched on some of the concepts of AOP, showing how to implement them using dynamic proxies in Java. Several other AOP concepts like join points (well-defined points in a program's execution, such as a method invocation or a specific exception being thrown), and point cuts (a set of join points) can also be implemented in your factory class. Please see Resources for more information.

If you need to use AOP concepts heavily throughout your application and can choose an existing framework, then choose a well-known, well-tested, and well-supported framework rather than build one yourself. If such a framework is not an option for you, consider building your own AOP framework. An AOP alliance has been formed that provides the interfaces for Java AOP to intercept method calls (see Resources for more information). Before building your own framework, look at the AOP alliance interfaces.

If you use an existing framework that does not provide AOP support and you need to implement a few AOP features, instead of introducing a full-blown AOP framework into the project, consider building a small framework on your own with generic dynamic proxies. Hopefully this article has provided some idea of how to achieve that. If you use an existing AOP framework that uses a dynamic proxy approach, then this article should help you understand the basics of dynamic proxies and their chaining.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值