Advanced Use of Pointcuts
In the last chapter, we looked at five basic Pointcut implementations Spring provides; for the most part, we found that these meet the needs of our applications. However, sometimes you need more flexibility when defining pointcuts. Spring provides two additional Pointcut implementations, ComposablePointcut and ControlFlowPointcut, that provide exactly the flexibility you need.
Using Control Flow Pointcuts
Spring control flow pointcuts, implemented by the ControlFlowPointcut class, are similar to the cflow construct available in many other AOP implementations, although they are not quite as powerful. Essentially, a control flow pointcut in Spring pointcuts all method calls below a given method or below all methods in a class. This is quite hard to visualize and is better explained using an example.
Listing 7-1 shows a SimpleBeforeAdvice that writes a message out describing the method it is advising.
Listing 7-1: The SimpleBeforeAdvice Class
package com.apress.prospring.ch7.cflow;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class SimpleBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target)
throws Throwable {
System.out.println("Before method: " + method);
}
}
This advice class allows us to see which methods are being pointcut by the ControlFlowPointcut. In Listing 7-2, you can see a simple class with one method—the method that we want to advise.
Listing 7-2: The TestBean Class
package com.apress.prospring.ch7.cflow;
public class TestBean {
public void foo() {
System.out.println("foo()");
}
}
In Listing 7-2, you can see the simple foo() method that we want to advise. We have, however, a special requirement—we only want to advise this method when it is called from another, specific method. Listing 7-3 shows a simple driver program for this example.
Listing 7-3: Using the ControlFlowPointcut Class
package com.apress.prospring.ch7.cflow;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ControlFlowPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class ControlFlowExample {
public static void main(String[] args) {
ControlFlowExample ex = new ControlFlowExample();
ex.run();
}
public void run() {
TestBean target = new TestBean();
// create advisor
Pointcut pc = new ControlFlowPointcut(ControlFlowExample.class, "test");
Advisor advisor = new DefaultPointcutAdvisor(pc,
new SimpleBeforeAdvice());
// create proxy
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
TestBean proxy = (TestBean) pf.getProxy();
System.out.println("Trying normal invoke");
proxy.foo();
System.out.println("Trying under ControlFlowExample.test()");
test(proxy);
}
private void test(TestBean bean) {
bean.foo();
}
}
In Listing 7-3, the advised proxy is assembled with ControlFlowPointcut and then the foo() method is invoked twice: once directly from the run() method and once from the test() method. Here is the line of particular interest:
Pointcut pc = new ControlFlowPointcut(ControlFlowExample.class, "test");
In this line, we are creating a ControlFlowPointcut instance for the test() method of the ControlFlowExample class. Essentially, this says, "pointcut all methods that are called from the ControlFlowExample.test() method." Note that although we said "pointcut all methods," in fact, this really means "pointcut all methods on the proxy object that is advised using the Advisor corresponding to this instance of ControlFlowPointcut." Running this example yields the following output:
Trying normal invoke
foo()
Trying under ControlFlowExample.test()
Before method: public void com.apress.prospring.ch7.cflow.TestBean.foo()
foo()
As you can see, when the foo() method is first invoked outside of the control flow of the test() method, it is unadvised. When it executes for a second time, this time inside the control flow of the test() method, the ControlFlowPointcut indicates that its associated advice applies to the method, and thus the method is advised. Note that if we had called another method from within the test() method, one that was not on the advised proxy, it would not have been advised.
Control flow pointcuts can be extremely useful, allowing you to advise an object selectively only when it is executed in the context of another. However, be aware that you take a substantial performance hit for using control flow pointcut over other pointcuts. Figures from the Spring documentation indicate that a control flow pointcut is typically five times slower than other pointcuts on a 1.4 JVM and ten times slower on a 1.3 JVM.
Using ComposablePointcut
In previous pointcutting examples, we used just a single pointcut for each Advisor. In most cases, this is usually enough, but in some cases, you may need to compose two or more point- cuts together to achieve the desired goal. Consider the situation where you want to pointcut all getter and setter methods on a bean. You have a pointcut for getters and a pointcut for setters, but you don't have one for both. Of course, you could just create another pointcut with the new logic, but a better approach is to combine the two pointcuts into a single pointcut using ComposablePointcut.
The ComposablePointcut supports two methods: union() and intersection(). By default, ComposablePointcut is created with a ClassFilter that matches all classes and a MethodMatcher that matches all methods, although you can supply your own initial ClassFilter and MethodMatcher during construction. The union() and intersection() methods are both overloaded to accept ClassFilter and MethodMatcher arguments.
Invoking the union() method for a MethodMatcher replaces the MethodMatcher of the ComposablePointcut with an instance of UnionMethodMatcher using the current MethodMatcher of the ComposablePointcut and the MethodMatcher passed to the union() method as arguments. The UnionMethodMatcher then returns true for a match if either of its wrapped MethodMatchers return true. You can invoke the union() method as many times as you want, with each call creating a new UnionMethodMatcher that wraps the current MethodMatcher of the ComposablePointcut with the MethodMatcher passed to union(). A similar structure is followed when you are using ClassFilter with the union() method.
Internally, the intersection() method works in a similar way to the union(). However, the IntersectionMethodMatcher class only returns true for a match if both of the embedded MethodMatchers return true for a match. Essentially, you can think of the union() method as an any match, in that it returns true if any of the matchers it is wrapping return true; and you can think of the intersection() method as an all match, in that it only returns true if all its wrapped matchers return true.
As with control flow pointcuts, this is quite difficult to visualize, and it is much easier to understand with an example. Listing 7-4 shows a simple bean with three methods.
Listing 7-4: The SampleBean Class
package com.apress.prospring.ch7.composable;
public class SampleBean {
public String getName() {
return "Rob Harrop";
}
public void setName(String name) {
}
public int getAge() {
return 100;
}
}
With this example, we are going to generate three different proxies using the same ComposablePointcut instance, but each time, we are going to modify the ComposablePointcut using either the union() or intersection() method. Following this, we will invoke all three methods on the SampleBean proxy and look at which ones have been advised. Listing 7-5 shows the code for this.
Listing 7-5: Investigating ComposablePointcut
package com.apress.prospring.ch7.composable;
import java.lang.reflect.Method;
import org.springframework.aop.Advisor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcher;
import com.apress.prospring.ch7.cflow.SimpleBeforeAdvice;
public class ComposablePointcutExample {
public static void main(String[] args) {
// create target
SampleBean target = new SampleBean();
ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE,
new GetterMethodMatcher());
System.out.println("Test 1");
SampleBean proxy = getProxy(pc, target);
testInvoke(proxy);
System.out.println("Test 2");
pc.union(new SetterMethodMatcher());
proxy = getProxy(pc, target);
testInvoke(proxy);
System.out.println("Test 3");
pc.intersection(new GetAgeMethodMatcher());
proxy = getProxy(pc, target);
testInvoke(proxy);
}
private static SampleBean getProxy(ComposablePointcut pc, SampleBean target) {
// create the advisor
Advisor advisor = new DefaultPointcutAdvisor(pc,
new SimpleBeforeAdvice());
// create the proxy
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
return (SampleBean) pf.getProxy();
}
private static void testInvoke(SampleBean proxy) {
proxy.getAge();
proxy.getName();
proxy.setName("Rob Harrop");
}
private static class GetterMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class cls) {
return (method.getName().startsWith("get"));
}
}
private static class GetAgeMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class cls) {
return "getAge".equals(method.getName());
}
}
private static class SetterMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class cls) {
return (method.getName().startsWith("set"));
}
}
}
The first thing to notice in this example is the set of three private MethodMatcher implementations. The GetterMethodMatcher matches all methods that start with get. This is the default MethodMatcher that we use to assemble the ComposablePointcut. Because of this, we expect that the first round of invocations on the SampleBean methods will result in only the getAge() and getName() methods being advised.
The SetterMethodMatcher matches all methods that start with set, and it is combined with the ComposablePointcut using union() for the second round of invocations. At this point, we have a union of two MethodMatchers: one that matches all methods starting with get and one that matches all methods starting with set. To this end, we expect that all invocations during the second round will be advised.
The GetAgeMethodMatcher is very specific and only matches the getAge() method. This MethodMatcher is combined with the ComposablePointcut using intersection() for the third round for invocations. Because the GetAgeMethodMatcher is being composed using intersection(), the only method that we expect to be advised in the third round of invocations is getAge(), because this is the only method that matches all the composed MethodMatchers.
Running this example results in the following output:
Test 1
Before method: public int com.apress.prospring.ch7.composable.SampleBean.getAge()
Before method: public java.lang.String ¿
com.apress.prospring.ch7.composable.SampleBean.getName()
Test 2
Before method: public int com.apress.prospring.ch7.composable.SampleBean.getAge()
Before method: public java.lang.String ¿
com.apress.prospring.ch7.composable.SampleBean.getName()
Before method: public void
com.apress.prospring.ch7.composable.SampleBean.setName(java.lang.String)
Test 3
Before method: public int com.apress.prospring.ch7.composable.SampleBean.getAge()
As expected, the first round of invocations on the proxy saw only the getAge() and getName() methods being advised. For the second round, when the SetterMethodMatcher had been composed with the union() method, all methods were advised. In the final round, as a result of the intersection of the GetAgeMethodMatcher, only the getAge() method was advised.
Although this example only demonstrated the use of MethodMatchers in the composition process, it is just as simple to use ClassFilter when you are building the pointcut. Indeed, you can use a combination of MethodMatchers and ClassFilters when building your composite pointcut.
Composition and the Pointcut Interface
In the last section, you saw how to create a composite pointcut using multiple MethodMatchers and ClassFilters. You can also create composite pointcuts using other objects that implement the Pointcut interface. You can perform an intersection of Pointcuts using the ComposablePointcut.intersection() method, but for a union, you need to use the
org.springframework.aop.support.Pointcuts class that has both intersection() and union() methods.
Composition of Pointcuts works in the same way as it does for MethodMatchers, so we do not go into any detail here. You can find more information about composition by reading the JavaDoc for the Pointcuts class.
Pointcutting Summary
From the discussions in this chapter and in the previous chapter, you can see that Spring offers a powerful set of Pointcut implementations that should meet most, if not all, of your application's requirements. Remember that if you can't find a pointcut to suit your needs, you can create your own implementation from scratch by implementing Pointcut, MethodMatcher, and ClassFilter.
There are two patterns you use to combine pointcuts and advisors together. The first pattern, the one that we have used so far, involves having the pointcut implementation decoupled from the advisor. In the code we have seen up to this point, we have created instances of Pointcut implementations and then used the DefaultPointcutAdvisor to add advice along with the Pointcut to the proxy.
The second option, one that is adopted by many of the examples in the Spring documentation, is to encapsulate the Pointcut inside your own Advisor implementation. This way, you have a class that implements both Pointcut and PointcutAdvisor, with the PointcutAdvisor.getPointcut() method simply returning this. This is an approach many classes, such as StaticMethodMatcherPointcutAdvisor, use in Spring.
We find that the first approach is the most flexible, allowing you to use different Pointcut implementations with different Advisor implementations. However, the second approach is useful in situations where you are going to be using the same combination of Pointcut and Advisor in different parts of your application, or indeed, across many different applications. The second approach is useful when each Advisor must have a separate instance of a Pointcut; by making the Advisor responsible for creating the Pointcut, you can ensure that this is the case.
If you recall the discussion on proxy performance from the previous chapter, you will remember that unadvised methods perform much better than methods that are advised. For this reason, you should ensure that, by using Pointcuts, you only advise the methods that are absolutely necessary. This way you reduce the amount of unnecessary overhead added to your application by using AOP.
No comments:
Post a Comment