Aspect-Oriented Programming with ColdSpring
Aspect-Oriented Programming (AOP) may simultaneously be the most useful feature that ColdSpring offers and the most difficult to understand. In order to explain why it is useful, we first need to explain what it is. This section is a bit long, so bear with us. It's not an easy thing to explain.
What is AOP?
Many systems have behavior that applies across many different parts of the code. These may include logging, security, failure handling, or persistence, and tend to cut across many groups of functional components.
While they can be thought about and analyzed relatively separately from the basic functionality, programming them using current approaches tends to result in these aspects being spread throughout the code. The source code becomes a tangled mess of instructions for different purposes. It also results in extensive duplication of logic.
This "tangling" phenomenon is at the heart of much needless complexity in existing software systems. A number of researchers have begun working on approaches to this problem that allow programmers to express each of a system's aspects of concern in a separate and natural form, and then automatically combine those separate descriptions into a final executable form. These approaches have been called aspect-oriented programming.
That might have helped a little, but probably not enough. So keep that in your mind as we look at an example. Consider logging. A common requirement for an application. And it might sound easy to create. I could go into my code and into each method and add a call to a logging component, which will capture the name of the method and the arguments that were passed into it. It might write them to a database or a log file, but for this example let's avoid having to set up a database or open up log files and just say we're storing it in a request-scope variable so that we can do something with it later:
<cffunction name="reverseString" access="public" returntype="string" output="false"> <cfargument name="string" type="string" required="true" /> <!--- Log the method name and arguments. ---> <cfset getLogger().log('reverseString', arguments) /> <cfreturn Reverse(arguments.string) /> </cffunction>
OK, not so bad. Maybe. But as we do this to more methods...
<cffunction name="reverseString" access="public" returntype="string" output="false"> <cfargument name="string" type="string" required="true" /> <!--- Log the method name and arguments. ---> <cfset getLogger().log('reverseString', arguments) /> <cfreturn Reverse(arguments.string) /> </cffunction> <cffunction name="duplicateString" access="public" returntype="string" output="false"> <cfargument name="string" type="string" required="true" /> <cfargument name="numberOfDuplicates" type="numeric" required="true" /> <!--- Log the method name and arguments. ---> <cfset getLogger().log('duplicateString', arguments) /> <cfreturn RepeatString(arguments.string, arguments.numberOfDuplicates) /> </cffunction> <cffunction name="capitalizeString" access="public" returntype="string" output="false"> <cfargument name="string" type="string" required="true" /> <!--- Log the method name and arguments. ---> <cfset getLogger().log('capitalizeString', arguments) /> <cfreturn Ucase(arguments.string) /> </cffunction>
That's getting ugly fast. What if we have 100 components with 10 methods each? We have to do this 1000 times? Manually replacing each method name in the logging call? And what if the logic changes and we want to capture the result as well as the method name and arguments? If horror is sinking in, good. It should be.
Logging is a classic cross-cutting concern. That means that it applies in a fairly generic way across all the different parts of our code. This is where AOP comes in. I can create a single, generic CFC and name it LoggingAdvice. In it, I can have logic that will log the desired information. It looks like this:
<cfcomponent output="false" displayname="LoggingAdvice" hint="I advise service layer methods and apply logging." extends="coldspring.aop.MethodInterceptor"> <cffunction name="init" returntype="any" output="false" access="public" hint="Constructor"> <cfreturn this /> </cffunction> <cffunction name="invokeMethod" returntype="any" access="public" output="false" hint=""> <cfargument name="methodInvocation" type="coldspring.aop.MethodInvocation" required="true" hint="" /> <cfset var local = StructNew() /> <!--- Capture the arguments and method name being invoked. ---> <cfset local.logData = StructNew() /> <cfset local.logData.arguments = StructCopy(arguments.methodInvocation.getArguments()) /> <cfset local.logData.method = arguments.methodInvocation.getMethod().getMethodName() /> <cfset request.logData = local.logData /> <!--- Proceed with the method call to the underlying CFC. ---> <cfset local.result = arguments.methodInvocation.proceed() /> <!--- Return the result of the method call. ---> <cfreturn local.result /> </cffunction> </cfcomponent>
Once in place, I can tell ColdSpring that I want this Logging Advice to be executed whenever a method is called on my original component. What ColdSpring will do is create what is known as an AOP Proxy, and my original component is known as a "proxied component". In essence, ColdSpring fakes out the rest of my application. Everything that asks for my original component actually gets back this AOP proxy. They never know that anything unusual is going on. To the rest of the application, the AOP proxy IS the original component.
We won't get into the technical details of how this is done. Suffice to say that once it happens, we're free to run our Logging Advice or any other code we want to whenever anything tries to call the original component. You can see above that the Logging Advice stores the information it wants, then lets the method call proceed to the original component, and finally gets back the result from the original component and returns it. We can control exactly what happens before, during, and after the method call.
If this sounds vaguely like it could be really, really powerful, it is. In this simple example, the XML configuration might look verbose, but if you imagine being able to apply the advice to dozens of methods at a time instead of three, the tradeoff is excellent. Here is the XML:
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="languageService" class="coldspring.aop.framework.ProxyFactoryBean"> <property name="target"> <bean class="coldspring.examples.quickstart.components.LanguageService" /> </property> <property name="interceptorNames"> <list> <value>loggingAdvisor</value> </list> </property> </bean> <bean id="loggingAdvice" class="coldspring.examples.quickstart.components.LoggingAdvice" /> <bean id="loggingAdvisor" class="coldspring.aop.support.NamedMethodPointcutAdvisor"> <property name="advice"> <ref bean="loggingAdvice" /> </property> <property name="mappedNames"> <value>*</value> </property> </bean> </beans>
I'm creating a proxy for my LanguageService and stating that I want my LoggingAdvisor to be applied to it. The LoggingAdviser will be applied to all methods in my LanguageService because I used "*" for the "mapped names" property. (I could limit it by giving it a list of method names instead.) Whenever the Advisor finds a matching method name being called (in this case, any method name), it fires the LoggingAdvice.
OK, this has been long and you probably want to see it actually work. So let's test things out by firing up ColdSpring and making some calls to our LanguageService. We'll show the results, as well as dump out the request-scoped logging data that is captured by the Advice. The code is:
<cfset languageService = beanFactory.getBean('languageService') /> Result for duplicate: #languageService.duplicateString('foo', 3)# <cfdump var="#request.logData#" label="Log data for duplicate"> Result for reverse: #languageService.reverseString('ColdSpring')# <cfdump var="#request.logData#" label="Log data for reverse"> Result for capitalize: #languageService.capitalizeString('Dependency Injection')# <cfdump var="#request.logData#" label="Log data for capitalize">
Result for duplicate: foofoofoo
|Log data for duplicate - struct|
Result for reverse: gnirpSdloC
|Log data for reverse - struct|
Result for capitalize: DEPENDENCY INJECTION
|Log data for capitalize - struct|
It worked like a charm! The LoggingAdvice has been transparently and automatically applied to all of the methods in my LanguageService. And I never even had to touch the LanguageService itself to do it. ColdSpring handled all the magic of creating the AOP Proxy for my LanguageService, and firing the Advice when I called methods on the LanguageService.
If Your Brain Hurts...
That's OK. Depending on your background and experience, this may take some time to sink in. AOP is a really powerful idea but it is also rather unintuitive. For now, just absorb what you've seen so far. And if you're up for it, consider how AOP could be very useful for things like enforcing security, translating data (say into JSON, or to Flex), and much more.