In the last installment of this series I showed you how you can leverage Spring.NET's ProxyFactory to wrap an object with a proxy and add interceptors to this proxy in order to enrich the behavior of your object's methods without having to changing a single line of code. Nevertheless it is quite cumbersome to wrap each single object explicitely.
Describing the "Where To Apply"
What we are looking for is a more automated approach, where we just want to describe the objects or - more fine grained - methods that we want to have a particular behavior applied. An interceptor ("Advice" in AOP terms) only describes *what* to do (e.g. retrying an operation), thus we need a way to describe *where* to apply it. In AOP terms, every method that can be wrapped by an interceptor is called a Joinpoint.
Of course we can taxatively enumerate the list of joinpoints that we want a particular advice to be applied like this:
Apply RetryInterceptor At CalculatorWebService.Add CalculatorWebService.Divide ...
but this clearly is not what we really want as in larger applications this quickly requires us to list each and every joinpoint in this list. What want to say something along the lines of
Apply RetryInterceptor At All Service Methods
Looks much clearer, but how shall we describe that? You can compare it to selecting records from a database table. What we are dealing with here is a set of possible joinpoints and our "where" clause describes the characteristics of those joinpoints that we want to capture by our select statement. Applying this to our example, we can e.g. write
Apply RetryInterceptor At ClassName LIKE '*Service' AND MethodName LIKE '*'
In AOP terms, such a SELECT statement, selecting a subset of joinpoints out of all possible joinpoints, is called a Pointcut.
Applying Advices to Pointcuts: Entering Object Post-Processors
Now that we have an idea, how we may select a set of joinpoints based on some criteria, how can we apply this technically? Remember, that in Spring you describe your objects using object definitions, a sort of recipe, that tells Spring how to instantiate and configure a particular object. Here's an example of such construction recipes in XML:
<object name="Alice" type="Spring.Objects.TestObject"> <property name="age" value="31"/> </object> <object name="Bob" type="Spring.Objects.TestObject"> <property name="age" value="33"/> <property name="spouse" ref="Alice" /> </object>
Using such object definitions we can build up whole object graphs, Since instantiation of these objects happens under control of Spring, we can leverage one of Spring's extension points to manipulate an object once it gets instantiated. This extension point is called "object post-processing". Everytime an object gets instantiated, Spring hands this object to all configured objects that implement the interface IObjectPostProcessor:
This is exactly what we need! We can implement our own IObjectPostProcessor to wrap the target instances within a proxy as needed. A simple implementation using a predicate for deciding which objects to wrap looks like this:
public class CustomAutoProxyCreator : IObjectPostProcessor { private readonly Func<object, string, bool> matchesPointcut; private readonly IMethodInterceptor[] advices; public CustomAutoProxyCreator(Func<object, string, bool> matchesPointcut, params IMethodInterceptor[] advices) { this.matchesPointcut = matchesPointcut; this.advices = advices; } public object PostProcessBeforeInitialization(object instance, string objectName) { return instance; } public object PostProcessAfterInitialization(object instance, string objectName) { if (matchesPointcut(instance, objectName)) { ProxyFactory proxyFactory = new ProxyFactory(instance); foreach(var advice in advices) { proxyFactory.AddAdvice(advice); } return proxyFactory.GetProxy(); } return instance; } }
We can now add this CustomAutoProxyCreator to our configuration in order to apply caching and retry behaviors to every object, who's object name ends with "*Service":
[Configuration] public class AutoProxyDemoConfig { public CalculatorWebService TheCalculatorService() { return new CalculatorWebService(); } public CustomAutoProxyCreator AutoProxyCreator() { return new CustomAutoProxyCreator( (instance, name) => name.EndsWith("Service"), new CacheInterceptor(), new RetryInterceptor() ); } }
Reusing Spring.NET's AutoProxy Facilities - DefaultAdvisorAutoProxyCreator
Of course you don't need to implement your own IObjectPostProcessor for automatically creating AOP proxies. Spring.NET already comes with a predefined set of implementations that allow you to choose from various strategies for automatically creating proxies, DefaultAdvisorAutoProxyCreator (DAAPC) being the most flexible and powerful one.
What makes DAAPC so powerful? Remember our initial discussion about pointcuts and advices. DAAPC allows you to simply add such combinations of pointcuts and advices to the container configuration and will automatically pick them up to do it's magic. For this reason, Spring.NET introduced a special kind of object, a so called "Advisor". An advisor is nothing else than a combination of a pointcut and an advice to apply at this pointcut:
To leverage DAAPC, add a DAAPC and a list of advisors as needed to your container configuration. DAAPC then will automatically pickup these advisors and apply them. The following example demonstrates, how to apply our advices using this technique:
[Configuration] public class DefaultAdvisorAutoProxyDemoConfig { public CalculatorWebService TheCalculatorService() { return new CalculatorWebService(); } public DefaultAdvisorAutoProxyCreator AutoProxyCreator() { return new DefaultAdvisorAutoProxyCreator(); } public DefaultPointcutAdvisor CacheServiceCallResultsAdvisor() { return new DefaultPointcutAdvisor( new SdkRegularExpressionMethodPointcut(@".*Service\.*"), new CacheInterceptor() ); } public DefaultPointcutAdvisor RetryServiceCallResultsAdvisor() { return new DefaultPointcutAdvisor( new SdkRegularExpressionMethodPointcut(@".*Service\.*"), new RetryInterceptor() ); } }
The advantage of this technique is, that whenever you want to apply a new advice, you just need to add new advisors to your definition and DAAPC automatically will pick them up and apply them.
And now the end is near...
What a journey. We started by identifiying cross-cutting concerns, refactoring them into separate classes using the decorator pattern. By introducing interceptors we made those behaviors reusable across different target classes. Using ProxyFactory, we even don't have to write any proxy code anymore. Finally, using pointcuts and Spring.NET's AutoProxy facilities, we can easily apply advices on any set of objects as needed. By following common sense and step-by-step refactoring, we almost naturally ended up at this mysterious technique called Aspect-Oriented Programming and proofs that there is nothing magically behind this concept. Also it proofs, that Object-Oriented Programming and Aspect-Oriented Programming are not mutually exclusive, but perfectly work together to tame today's application's complexities.
Nevertheless we are not done yet. Until now we have only discussed the technical solution, how to implement AOP. In my next post I will discuss, when to use AOP and point out a couple of issues you have to consider when applying AOP.
As usual, you can download the sample code for this post.
3 comments:
very interesting and well explained topic. thanks for that
well said, I agree. While I'm not really against constructor injection, setter injection just seems like there is less friction. Also less work to refactor.
Thanks for the effort you put into these.
Post a Comment