Getting Started with Introductions

Introductions are an important part of the AOP feature set available in Spring. By using introductions, you can introduce new functionality to an existing object dynamically. In Spring, you can introduce an implementation of any interface to an existing object. You may well be wondering exactly why this is useful—why would you want to add functionality dynamically at runtime when you can simply add that functionality at development time? The answer to this question is easy. You add functionality dynamically when the functionality is crosscutting and is not easily implemented using traditional advice.

The Spring documentation gives two atypical examples of introduction use: object locking and modification detection. In the case of object locking, which is implemented in the Spring documentation, we have an interface, Lockable, that defines the method for locking and unlocking an object. The intended application for this interface involves enabling the application to lock an object so that its internal state cannot be modified. Now you could simply implement this interface manually for every class you wish to make lockable. However, this would result in a lot of duplicated code across many classes. Sure, you can refactor the implementation into an abstract base class, but then you lose your one shot at concrete inheritance, and you still have to check the lock status in every method that modifies the state of the object. Clearly this is not an ideal situation and has the potential to lead to many bugs, and no doubt some maintenance nightmares.

By using introductions, you can overcome all of these issues. Using an introduction, you can centralize the implementation of the Lockable interface into a single class, and then, at runtime, have any object you wish adopt this implementation of Lockable. Not only does the object adopt the implementation of Lockable but it also becomes an instance of Lockable, in that it passes the instanceof test for the Lockable interface, even though its class does not implement this interface.

Clearly this overcomes the problem of centralizing the implementation logic without affecting the concrete inheritance hierarchy of your classes, but what about all the code you need to write to check the lock status? Well, an introduction is simply an extension of a method interceptor, and as such, it can intercept any method on the object on which the introduction was made. Using this feature, you could check the lock status before any calls are made to setter methods and throw an Exception if the object is locked. All of this code is encapsulated in a single place, and none of the Lockable objects needs to be aware of this.

Introductions are key to providing declarative services in applications. For instance, if you build an application that is fully aware of the Lockable interface, by using introductions, you declaratively define exactly which objects should be made Lockable.

We won't be spending any more time looking at the Lockable interface and how to implement it using introductions because it is fully discussed in the Spring documentation. Instead we focus on the other, unimplemented, example from the documentation—object modification detection. However, before we start we will take a look at the basics behind building an introduction.

Introduction Basics

Spring treats introductions as a special type of advice, more specifically, as a special type of around advice. Because introductions apply solely at the class level, you cannot use pointcuts with introductions; semantically, the two don't match. An introduction adds new interface implementations to a class and a pointcut defines which methods an advice applies. You create an introduction by implementing the IntroductionInterceptor class, which extends the MethodInterceptor interface. Figure 7-1 shows this structure along with the methods of both interfaces.

image from book
Figure 7-1: Interface structure for introductions

As you can see, the MethodInterceptor interface defines an invoke() method. Using this method, you provide the implementation for the interfaces that you are introducing and perform interception for any additional methods as required. Implementing all methods for an interface inside a single method can prove troublesome, and it is likely to result in an awful lot of code that you will have to wade through just to decide which method to invoke. Thankfully, Spring provides a default implementation of IntroductionInterceptor, DelegatingIntroductionInterceptor, which makes creating introductions much simpler. To build an introduction using DelegatingIntroductionInterceptor, you create a class that both inherits from DelegatingIntroductionInterceptor and implements the interfaces you want to introduce. The DelegatingIntroductionInterceptor then simply delegates all calls to introduced methods to the corresponding method on itself. Don't worry if this seems a little unclear; you will see an example of it in the next section.

Just as you need to use a PointcutAdvisor when you are working with pointcut advice, you need to use an IntroductionAdvisor to add introductions to a proxy. The default implementation of IntroductionAdvisor is DefaultIntroductionAdvisor, which should suffice for most, if not all, of your introduction needs. You should be aware that adding an introduction using ProxyFactory.addAdvice() is not permitted and results in an AopConfigException being thrown.

When using standard advice—that is, not introductions—it is possible for the same advice instance to be used for many different objects. The Spring documentation refers to this as the per-class lifecycle, although you can use a single advice instance for many different classes. For introductions, the introduction advice forms a part of the state of the advised object, and as a result, you must have a distinct advice instance for every advised object. This is called the per-instance lifecycle. Because you must ensure that each advised object has a distinct instance of the introduction, it is often preferable to create a subclass of DefaultIntroductionAdvisor that is responsible for creating the introduction advice. This way, you only need to ensure that a new instance of your advisor class is created for each object, because it will automatically create a new instance of the introduction.

That covers the basics of introduction creation. We will now move on to discuss how you can use introductions to solve the problem of object modification detection.

Object Modification Detection with Introductions

Object modification detection is a useful technique for many reasons. Typically you apply modification detection to prevent unnecessary database access when you are persisting object data. If an object is passed to a method for modification but it comes back unmodified, there is little point in issuing an update statement to the database. Using a modification check in this way can really increase application throughput, especially when the database is already under a substantial load or is located on some remote network making communication an expensive operation.

Unfortunately, this kind of functionality is difficult to implement by hand because it requires you to add to every method that can modify object state to check if the object state is actually being modified. When you consider all the null checks that have to be made and the checks to see if the value is actually changing, you are looking at around eight lines of code per method. You could refactor this into a single method, but you still have to call this method every time you need to perform the check. Spread this across a typical application with many different classes that require modification checks and you have a disaster waiting to happen.

This is clearly a place where introductions will help. We do not want to have to make it so each class that requires modification checks inherits from some base implementation, losing its only chance for inheritance as a result, nor do we really want to be adding checking code to each and every state-changing method. Using introductions, we can provide a flexible solution to the modification detection problem without having to write a bunch of repetitive, error- prone code.

In this example, we are going to build a full modification check framework using introductions. The modification check logic is encapsulated by the IsModified interface, an implementation of which will be introduced into the appropriate objects, along with interception logic to perform modification checks automatically. For the purposes of this example, we use JavaBeans conventions, in that we consider a modification to be any call to a setter method. Of course, we don't just treat all calls to a setter method as a modification—we check to see if the value being passed to the setter is different from the one currently stored in the object. The only flaw with this solution is that setting an object back to its original state will still reflect a modification if any one of the values on the object changed. However, the implementation here is nontrivial and suffices for most requirements. Implementing the more complete solution would result in an overly complex example.

The IsModified Interface

Central to the modification check solution is the IsModified interface, which our fictional application uses to make intelligent decisions about object persistence. We do not look at how the application would use IsModified; instead we focus on the implementation of the introduction.

Listing 7-6 shows the IsModified interface.

Listing 7-6: The IsModified Interface

image from book
package com.apress.prospring.ch7.introductions;      public interface IsModified {          public boolean isModified(); }
image from book

Nothing special here—just a single method, isModified(), indicating whether or not an object has been modified.

Creating a Mixin

The next step is to create the code that implements IsModified and that is introduced to the objects; this is referred to as a mixin. As we mentioned earlier, it is much simpler to create mixins by subclassing DelegatingIntroductionInterceptor rather than to create one directly using IntroductionInterceptor. Our mixin class, IsModifiedMixin, subclasses DelegatingIntroductionInterceptor and also implements the IsModified interface. This is shown in Listing 7-7.

Listing 7-7: The IsModifiedMixin Class

image from book
package com.apress.prospring.ch7.introductions;      import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;      import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.support.DelegatingIntroductionInterceptor;      public class IsModifiedMixin extends DelegatingIntroductionInterceptor         implements IsModified {          private boolean isModified = false;          private Map methodCache = new HashMap();          public boolean isModified() {         return isModified;     }          public Object invoke(MethodInvocation invocation) throws Throwable {              if (!isModified) {             if ((invocation.getMethod().getName().startsWith("set"))                     && (invocation.getArguments().length == 1)) {                      // invoke the corresponding get method to see                 // if the value has actually changed                 Method getter = getGetter(invocation.getMethod());                      if (getter != null) {                     // modification check is unimportant                     // for write only methods                     Object newVal = invocation.getArguments()[0];                     Object oldVal = getter.invoke(invocation.getThis(), null);                                          if((newVal == null) && (oldVal == null)) {                         isModified = false;                     } else if((newVal == null) && (oldVal != null)) {                         isModified = true;                     } else if((newVal != null) && (oldVal == null)) {                         isModified = true;                     } else {                         isModified = (!newVal.equals(oldVal));                     }                 }                  }         }              return super.invoke(invocation);     }          private Method getGetter(Method setter) {         Method getter = null;              // attempt cache retrieval.         getter = (Method) methodCache.get(setter);              if (getter != null) {             return getter;         }              String getterName = setter.getName().replaceFirst("set", "get");         try {             getter = setter.getDeclaringClass().getMethod(getterName, null);                  // cache getter             synchronized (methodCache) {                 methodCache.put(setter, getter);             }                  return getter;         } catch (NoSuchMethodException ex) {             // must be write only             return null;         }     } }
image from book

The first thing to notice here is the implementation of IsModified, which is made up of the private modified field and the isModified() method. This example highlights why you must have one mixin instance per advised object—the mixin introduces not only methods to the object but also state. If you share a single instance of this mixin across many different objects, then you are also sharing the state, which means all objects show as modified the first time a single object becomes modified.

You do not actually have to implement the invoke() method for a mixin, but in this case, doing so allows us to detect automatically when a modification occurs. The implementation for the modification check is fairly trivial. We start by only performing the check if the object is still unmodified; we do not need to check for modifications once we know that the object has been modified. Next, we check to see if the method is a setter, and if it is, we retrieve the corresponding getter method. Note that we cache the getter/setter pairs for quicker future retrieval. Finally, we compare the value returned by the getter with that passed to the setter to determine whether or not a modification has occurred. Notice that we check for the different possible combinations of null and set the modifications appropriately. It is important to remember that when you are using DelegatingIntroductionInterceptor, you must call super.invoke() when overriding invoke() because it is the DelegatingIntroductionInterceptor that actually dispatches the invocation to the correct location, either the advised object or the mixin itself.

You can implement as many interfaces as you like in your mixin, each of which is automatically introduced into the advised object. If you need to implement an interface on the mixin and you do not want to introduce that interface, you can simply call suppressInterface() from the mixin constructor after invoking super(), to prevent that interface from being exposed.

Creating an Advisor

The next step is to create an Advisor to wrap the creation of the mixin class. This step is optional, but it does help ensure that a new instance of the mixin is being used for each advised object. Listing 7-8 shows the IsModifiedAdvisor() class.

Listing 7-8: Creating an Advisor for Your Mixin

image from book
package com.apress.prospring.ch7.introductions;      import org.springframework.aop.support.DefaultIntroductionAdvisor;      public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {          public IsModifiedAdvisor() {         super(new IsModifiedMixin());     } }
image from book

Notice that we have extended the DefaultIntroductionAdvisor to create our IsModifiedAdvisor. The implementation of this advisor is trivial and self-explanatory.

Putting It All Together

Now that we have a mixin class and an Advisor class, we can test out the modification check framework. Listing 7-9 shows a simple class that we use to test out the IsModifiedMixin.

Listing 7-9: The TargetBean Class

image from book
package com.apress.prospring.ch7.introductions;      public class TargetBean {          private String name;          public void setName(String name) {         this.name = name;     }          public String getName() {         return name;     } } 
image from book

This bean has a single property, name, that we use when we are testing the modification check mixin. Listing 7-10 shows how to assemble the advised proxy and then tests the modification check code.

Listing 7-10: Using the IsModifiedMixin

image from book
package com.apress.prospring.ch7.introductions;      import org.springframework.aop.IntroductionAdvisor; import org.springframework.aop.framework.ProxyFactory;      public class IntroductionExample {          public static void main(String[] args) {         // create the target         TargetBean target = new TargetBean();         target.setName("Rob Harrop");              // create the advisor         IntroductionAdvisor advisor = new IsModifiedAdvisor();              // create the proxy         ProxyFactory pf = new ProxyFactory();         pf.setTarget(target);         pf.addAdvisor(advisor);         pf.setOptimize(true);                  TargetBean proxy = (TargetBean)pf.getProxy();         IsModified proxyInterface = (IsModified)proxy;                  // test interfaces         System.out.println("Is TargetBean?: " + (proxy instanceof TargetBean));         System.out.println("Is IsModified?: " + (proxy instanceof IsModified));                  // test is modified implementation         System.out.println("Has been modified?: " + proxyInterface.isModified());         proxy.setName("Rob Harrop");         System.out.println("Has been modified?: " + proxyInterface.isModified());         proxy.setName("Joe Schmoe");         System.out.println("Has been modified?: " + proxyInterface.isModified());           } } 
image from book

Notice that when we are creating the proxy, we set the optimize flag to true to force the use of the CGLIB proxy. The reason for this is that when you are using the JDK proxy to introduce a mixin, the resulting proxy will not be an instance of the object class (in this case TargetBean); the proxy only implements the mixin interfaces, not the original class. With the CGLIB proxy, the original class is implemented by the proxy along with the mixin interfaces.

Notice in the code that we test first to see if the proxy is an instance of TargetBean and then to see if it is an instance of IsModified. Both tests return true when you are using the CGLIB proxy, but only the IsModified test returns true for the JDK proxy. Finally, we test the modification check code by first setting the name property to its current value and then to a new value, checking the value of the isModified flag each time. Running this example results in the following output:

Is TargetBean?: true Is IsModified?: true Has been modified?: false Has been modified?: false Has been modified?: true

As expected, both instanceof tests return true. Notice that the first call to isModified(), before any modification occurred, returns false. The next call, after we set the value of name to the same value, also returns false. For the final call, however, after we set the value of name to a new value, the isModified() method returns true, indicating that the object has in fact been modified.

Introductions Summary

Introductions are one of the most powerful features of Spring AOP; they allow you not only to extend the functionality of existing methods, but also to extend the set of interfaces and object implements dynamically. Using introductions is the perfect way to implement crosscutting logic that your application interacts with through well-defined interfaces. In general, this is the kind of logic that you want to apply declaratively rather than programmatically. By using the IsModifiedMixin defined in this example and the framework services discussed in the next section, we can declaratively define which objects are capable of modification checks, without needing to modify the implementations of those objects.

Obviously, because introductions work via proxies, they add a certain amount of overhead, and all methods on the proxy are considered to be advised, because you cannot use pointcuts in conjunction with introductions. However, in the case of many of the services you can implement using introductions such as the object modification check, this performance overhead is a small price to pay for the reduction in code required to implement the service, as well the increase in stability and maintainability that comes from fully centralizing the service logic.



Pro Spring
Pro Spring
ISBN: 1590594614
EAN: 2147483647
Year: 2006
Pages: 189

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net