Tuesday, November 15, 2005

ColdSpring AOP Tutorial – Part Two, Around Advice

First off, sorry for the long delay in the AOP tutorials. I’ve been working pretty hard on the aop framework for a new release, and I don’t want to blog about features that are not yet available. Since we are going to do a 0.5 release this week, though, it’s time to get back on track. One thing I need to point out is that you will need to get the latest BER from CVS to try these examples, unless you already have the 0.5 release. Ok, so lets get back to where we left off. We previously talked about writing Before Advice to provide logging services to CatalogDAO. Although I didn’t discuss After Advice, we could have accomplished the same thing, with the added ability to log information about the return values from the method calls in our loggingAdvice by extending coldspring.aop. AfterReturningAdvice and putting the logging code in the method ‘afterReturning()’ instead of ‘before()’. Lets take a quick look at what that advice object would look like:

<cfcomponent name="loggingAfterAdvice" extends="coldspring.aop.AfterReturningAdvice">

  <cffunction name="setLoggingService" returntype="void" access="public" output="false" hint="Dependency: security service">
    <cfargument name="loggingService" type="net.klondike.service.LoggingService" required="true" />
    <cfset variables.m_loggingService = arguments.loggingService />
  </cffunction>
  <cffunction name="getLoggingService" returntype="net.klondike.service.LoggingService" access="public" output="false" hint="Dependency: security service">
    <cfreturn variables.m_loggingService />
  </cffunction>

  <cffunction name="afterReturning" access="public" returntype="void">
    <cfargument name="returnVal" type="any" required="false" />
    <cfargument name="method" type="coldspring.aop.Method" required="false" />
    <cfargument name="args" type="struct" required="false" />
    <cfargument name="target" type="any" required="false" />

    <cfset var rtnString = ‘’ >
    <cfset var arg = '' />
    <cfset var argString = '' />
    <cfset var objName = getMetadata(arguments.target).name />

    <cfif StructKeyExists(arguments,’returnVal’)>
      <cfif isStruct(arguments.returnVal)>
      <cfset rtnString = “ returned a struct with” & StructCount(arguments.returnVal) & “ values.” />
    <cfelseif isArray(arguments.returnVal)>
      <cfset rtnString = “ returned an array with ” & ArrayLen(arguments.returnVal) & “ values.” />
    <cfelseif isObject(arguments.returnVal)>
      <cfset rtnString = “ returned a record cfc ” />
    <cfelse>
    <cfset rtnString = “ returned ” & arguments.returnVal />
    </cfif>
  </cfif>

    <cfset variables.m_loggingService.info("[" & objName & "] " & method.getMethodName() & "() " & rtnString) />

  </cffunction>

</cfcomponent>


A few things to note here, we have to check to see if ‘returnVal’ actually exists in arguments before using it, because if the method you are advising does not return a value, ‘returnVal’ will not exist. Also, and this differs from the early alpha release, it is not necessary to return ‘returnVal’ if it exists. This value is available for you to inspect only. If you need to alter the result of method invocation, you should be working with Around Advice. In order to configure this Advice, we would take the same steps as we did with the Before Advice, configure the NamedMethodPointcutAdvisor with this Advice, and configure the ProxyFactoryBean with the CatalogDao as the target.

So now that I have covered the more simple types of Advice, let’s look at the more powerful Around Advice, called MethodInterceptor. First off, let me explain some of the motivation behind the naming convention. There is an organization called the Aopalliance that has provided standard interfaces to method interception for the development of aop frameworks. The idea being that as long as developers adhere to these interfaces, your code will be portable across different aop implementations. That sounds like a good idea to me, since who knows if someone is going to come along and write a much better aop framework that I have done. So that being said, MethodInterceptor is a type of Around Advice, meaning that it allows you to place code before and after the method call, and gives you complete control of actually calling the method. So let’s see how this works in practice, and while we’re at it I’ll cover some of the api available in the framework. This time I’m going to create an Advice to cache cfcs returned from the dao’s fetch() method, looking first to see if they exist there, and also update the cache in the save() method. This implementation is pretty simplistic and should not be viewed as a real world example, but hopefully it will get your mind going. First lets review the CatalogDAO cfc so know what methods we are going to be advising.

<cfcomponent name="CatalogDAO" output="false">

  <cffunction name="init" access="public" returntype="Any" output="false">
    <cfreturn this />
  </cffunction>

  <cffunction name="setDataSource" returntype="void" access="public" output="false" hint="Dependency: datasource name">
    <cfargument name="dsn" type="string" required="true" />
    <cfset variables.m_dsn = arguments.dsn />
  </cffunction>

  <cffunction name="fetch" returntype=" net.klondike.component.Record" access="public" output="false">
    <cfargument name="recordID" type="numeric" required="true" />
    <cfset var record = CreateObject('component','net.klondike.component.Record').init() />
    <cfset var qrySelect = 0 />

    <cfquery name="qrySelect" maxrows="1" datasource="#variables.m_dsn#">
    SELECT QUERY …
    </cfquery>

    <cfif qrySelect.RecordCount>
      <cfset record.setRecordID(qrySelect.recordID) />
      <cfset record.setArtistID(qrySelect.artistID) />
      <cfset record.setGenreID(qrySelect.genreID) />
      <cfset record.setTitle(qrySelect.title) />
      <cfset record.setReleaseDate(qrySelect.releaseDate) />
      <cfset record.setImage(qrySelect.image) />
      <cfset record.setPrice(qrySelect.price) />
      <cfset record.setFeatured(qrySelect.featured) />
    </cfif>
    <cfreturn record />
  </cffunction>

  <cffunction name="create" returntype="void" access="public" output="false">
    <cfargument name="record" type="net.klondike.component.Record" required="true" />
    <cfset var qryInsert = 0 />

    <cfquery name="qryInsert" datasource="#variables.m_dsn#">
    INSERT QUERY …
    </cfquery>
  </cffunction>

  <cffunction name="update" returntype="void" access="public" output="false">
    <cfargument name="record" type="net.klondike.component.Record" required="true" />
    <cfset var qryUpdate = 0 />

    <cfquery name="qryUpdate" datasource="#variables.m_dsn#">
    UPDATE QUERY …
    </cfquery>
  </cffunction>

</cfcomponent>


Obviously a little SQL has been removed, and I’ve also included a change in the fetch() method. We are now going to create a Record cfc from the data in the query and return that instead of a query object, so we can cache it. OK, so lets look at the around advice.

<cfcomponent name="simpleCachingAdvice" extends="coldspring.aop.MethodInterceptor">

  <cfset variables.objectCache = StructNew() />
  <cfset variables.cacheTime = 45 />  

  <cffunction name="invokeMethod" access="public" returntype="any">
    <cfargument name="mi" type="coldspring.aop.MethodInvocation" required="true" />

    <cfset var args = arguments.mi.getArguments() />
    <cfset var methodName = arguments.mi.getMethod().getMethodName() />
    <cfset var record = 0 />
    <cfset var rtn = 0 />

    <cfif methodName IS 'fetch'>

      <cfif StructKeyExists(variables.objectCache, args['recordID']) and (DateDiff("m", variables.objectCache[args['recordID']].cached, Now()) LT 45) >
        <cflock name="simpleCachingAdvice" timeout="5">
          <cfset record = variables.objectCache[args['recordID']].obj />
        </cflock>
      <cfelse>
        <cfset record = arguments.mi.proceed() />
        <cflock name="simpleCachingAdvice" timeout="5">
          <cfif StructKeyExists(variables.objectCache, args['recordID']>
            <cfset variables.objectCache[args['recordID']] = StructNew() />
          <cfif>
          <cfset variables.objectCache[args['recordID']].cached = Now() />
          <cfset variables.objectCache[args['recordID']].obj = record />
        </cflock>
      </cfif>

      <cfreturn record />

    <cfelseif methodName IS 'save'>
      <cflock name="simpleCachingAdvice" timeout="5">
        <cfif StructKeyExists(variables.objectCache, args['record'].getRecordID() />
          <cfset variables.objectCache[args['record'].getRecordID()] = StructNew() />
        </cfif>
        <cfset variables.objectCache[args['record'].getRecordID()].cached = Now() />
        <cfset variables.objectCache[args['record'].getRecordID()].obj = args['record'] />
      </cflock>
      <cfreturn arguments.mi.proceed() />
    <cfelse>
      <cfreturn arguments.mi.proceed() />
    </cfif>
  </cffunction>

  <cffunction name="flushCache" access="public" returntype="void" output="false">
    <cflock name="simpleCachingAdvice" timeout="5">
      <cfset StructClear(variables.objectCache) />
    </cflock>
  </cffunction>

</cfcomponent>


So let’s examine what’s going on here. First we extend coldspring.aop.MethodInterceptor and overwrite invokeMethod() which receives an argument of type coldspring.aop.MethodInvocation. MethodInvocation will take care of moving through the chain of any Advice defined and then call the proxied method, by calling its proceed() method. The other public methods for MethodInvocation are getArguments() which returns the arguments you sent to the proxied method, getMethod() which returns a Method object which represents the actual method call, and getTarget() which returns the proxied object itself. From the Method cfc returned from getMethod(), getMethodName() is available as well, so you can see you pretty much have full access to the method call. In invokeMethod() above, the first thing I do is set some local variables to getArguments() and getMethod().getMethodName(), retrieving the argument collection and the name of the method. If the method name is ‘save’, I look in the local cache to see if a Record cfc exists that hasn’t expired, and if it does, I return that object instead of proceeding with the method call. If the Record instance doesn’t exist in the cache I store the result of calling proceed() in the cache. The ‘fetch’ method similarly will store the result of proceed(), and all other methods simply call proceed() without performing any additional work. There’s a very important point to take care to understand here. When using around advice, you are responsible for calling proceed() to get the method to execute and you are also responsible for returning the result of that call if necessary. This completely differs from using Before or After advice, where neither the method call or the return are your responsibility. This obviously makes Around Advice far more powerful, but you may want to think twice before using it if you don’t need to alter the method call itself. One other thing to take note of, if this particular Advice is not the last Around Advice configured, than any other Around Advice will NOT execute when I retrieve the Record cfc from the cache, because of the fact that I am not calling proceed(). This is another pitfall to be very weary of when designing apps using this technology!

So lets move on to the configuration. This time I am going simplify things a bit by skipping the setup of the Advisor all together. If you don’t care about matching the methods in a target, because your intension is to match all methods as you would with the ‘*’ pattern, you can just reference the Advice directly in the interceptorNames list for the ProxyFactoryBean. This will internally create a DefaultPointcutAdvisor for you, which will match all methods in the target. The resulting configuration file will look like the following.

<!-- set up the security advisor -->
<bean id="simpleCachingAdvice"
  class="net.klondike.aspects.simpleCachingAdvice" />

<!-- set up a proxy for the dao -->
<bean id="catalogDAOTarget"
  class="net.klondike.component.catalogDAO">
  <property name="dsn">
    <value>klondike</value>
  </property>
</bean>
  
<bean id="catalogDAO"
  class="coldspring.aop.framework.ProxyFactoryBean">
  <property name="target">
    <ref bean="catalogDAOTarget" />
  </property>
  <property name="interceptorNames">
    <list>
      <value>simpleCachingAdvice</value>
     </list>
  </property>
</bean>


You can see that this is a little more convenient, but there will be a bit of overhead in the fact that the all method calls will be processed through the interceptor instead of just the methods that actually use the cache. You may also have noticed that I also added a method to my advice that is not directly used by the aop framework, flushCache(). Just because an advice object is used by the aop framework doesn’t mean it’s not a full fledged business object! You can still retrieve the simpleCachingAdvice from the coldspring bean factory with a call to bf.getBean(“simpleCachingAdvice”) and call flushCache() in some other object in your application. This gives you an enormous amount of flexibility and power. Think about counting method calls. Have you ever put a tracking system in your apps to keep track how many users are in the system and the last thing they did? How many places in your app would that code exist, more importantly how easy would it be to turn off that system? Definitely a candidate for aop! Well I hope that I’ve sparked some more interest and brought some more of the details to light. Look for the next installment where I will cover building a suite of throws advice with the new After Throwing object, and I’ll try not to let so much time go by this time!

2 Comments:

Anonymous Anonymous said...

Nice tutorial Chris,

Keep them coming. I had a similiar concept in my own home grown framework, whereby i made a CF.util.Delegate which works similiar to ProxyFactoryBean but dispatches events before and after execution but none the less, the similiarities exist.

I've been a fan of SpringFramework since a Java dev showed me how agile it really is. My only demon has been not fully understanding it enough to have a gow at writing it for Coldfusion, but thanks to your brain, i can sit back and let you suckers do all the hardwork hehehehe.

Kudos though, great work.

3:36 AM  
Blogger Denny said...

This stuff is awesome. Thanks for the examples, and the code!

Woohoo!

4:02 PM  

Post a Comment

<< Home