Saturday, October 08, 2005

ColdSpring AOP Tutorial – Part One

Since I have already given an overview of ColdSpring’s AOP concepts, I’m going to just dive right into things. For part one of the developing with ColdSpring AOP, I’ll show how you can use a simple form of Advice called Before Advice to provide logging services to a model component. These examples are from the Klondike Records application that I showed at the frameworks conference last week, which we will also be releasing as an example application. I’ll show you how to define an Advice cfc, which will extends coldspring.aop.BeforeAdvice, how to define Pointcuts via Advisors in a standard ColdSpring config file, and how to use coldspring.aop.ProxyFactoryBean to wrap the Advice around methods of a target object by creating a Proxy object, containing the target. Think about the proxy object like an egg. The object you want to apply logging to would be the yoke and the Advice that provides the logging functionality is the egg white surrounding it. When the proxy factory creates this egg, and this is the great part, everything in you application thinks that it’s the same object that’s in the yoke! OK, so so let’s get right into it. First we’ll define a model component, which will be called CatalogDAO.cfc which we’ll then add logging services to.

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

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

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

  <!--- fetch a record --->
  <cffunction name="fetch" returntype="query" access="public" output="false">
    <cfargument name="recordID" type="numeric" required="true" />
    <cfquery name="qrySelect" maxrows="1" datasource="#variables.m_dsn#">
    SELECT QUERY …
    </cfquery>
    <cfreturn qrySelect />
  </cffunction>

  <!--- insert record --->
  <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>

  <!--- update record --->
  <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 we’re skipping some details, but the point is that this is just a standard model component; we don’t extend anything or put any special code here. The component should be completely usable with or without AOP. In my example app you will see that this dao is used as part of a service component, which is used by the catalogListener in the mach-ii app. The nice thing about using AOP to provide logging is that neither the dao nor the service component that uses it will have any code in them pertaining to logging. They are only concentrated on fetching and storing records, cohesion. I like to use log4j, so I’ll create a LoggingService cfc to handle the configuration details and provide a common api to my Aspects. Also, if I want to switch this out to use cflog or another implementation, I can do that in one place. So here’s the LoggingService cfc, but I’ll skip showing how to actually configure log4j, since I have already shown that in an earlier post. This service basically just retrieves the logger and wraps its info() method.

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

  <cffunction name="init" returntype="net.klondike.service.LoggingService" access="public" output="false">
    <cfset var category = CreateObject("java", "org.apache.log4j.Category") />
    <cfset variables.logger = category.getInstance('net.klondike') />
    <cfreturn this />
  </cffunction>

  <cffunction name="info" access="public" returntype="void" output="false">
    <cfargument name=message" type="string" required="true" />
    <cfif variables.logger.isInfoEnabled()>
    <cfset variables.logger.info(arguments.message) />
    </cfif>
  </cffunction>

</cfcomponent>


Ok, now onto the AOP part. I will first create a Before Advice to retrieve information about the current method being invoked and send it off as the message argument to the logging service’s info() method. Here’s the loggingBeforeAdvice cfc.

<cfcomponent name="loggingBeforeAdvice" extends="coldspring.aop.BeforeAdvice">

  <!--- setters for dependencies --->
  <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>

  <!--- before() is called by the aop framework before method invocation --->
  <cffunction name="before" access="public" returntype="any">
    <cfargument name="method" type="coldspring.aop.Method" required="true" />
    <cfargument name="args" type="struct" required="true" />
    <cfargument name="target" type="any" required="true" />

    <cfset var arg = '' />
    <cfset var argString = '' />
    <cfset var objName = getMetadata(arguments.target).name />
    <cfloop collection="#args#" item="arg">
      <cfif isSimpleValue(args[arg])>
        <cfif len(argString)>
          <cfset argString = argString & ', ' />
        </cfif>
        <cfset argString = argString & arg & '=' & args[arg] >
      </cfif>
    </cfloop>

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

  </cffunction>

</cfcomponent>


This probably looks more complicated that it really is, so I’ll walk through it. First off we extend coldspring.aop.BeforeAdvice, which provides typing for the aop framework, and the ‘before’ method which you will overwrite. Next we have a setter and a getter which ColdSpring will use to inject the LoggingService, and the AOP framework will use to clone the Advice. The heart of this cfc, however, is the before() method, which gets passed a Method object, responsible for handling the actual method call, the argument collection passed to the method, and the target object which receives the method call. You can see that you have an amazing amount of power at this point. But all I’m going to do here is get the name of the target object through metadata, the name of the method from the Method object, and I’ll loop through the arguments to find simple values that I can print as strings. From this I make a string to pass to my loggingService that will look something like this: ‘[net.klondike.component.catalogGateway] getGenres() called!’. One important thing to note here is that Before Advice can alter the arguments going into the method at this point, but is not responsible for actually executing the method, even though it does have access to it. There is another type of Advice that should be used if you want control over method execution, called Around Advice, but I will delay that discussion until part two. Here’s where the advice of ‘be really careful’ comes in. No other object in you application has any idea of what is going on at this point, so you can really cause havoc, so be careful!

Ok, so now we have a DAO that we want to apply logging to, a simple loggingService, and a Before Advice to apply the logging service with. Now this may seem like a lot of work to add logging to a method, and that’s true. But this Advice can be applied to ALL you model components without writing any more code, logging method calls across your entire application. All that’s left is configuring these components. As I wrote in my earlier post, this is a matter of identifying Join Points, methods that you want to log in objects, which is done through Pointcuts, with an Advisor. An Advisor is basically a collection of methods and advice to apply to them. OK, so here’s the ColdSpring config file that will tie all this together.

<beans>

  <!—define the logging service -->
  <bean id="loggingService"
class="net.klondike.component.LoggingService" />

  <!--define the loggingBeforeAdvice and set its logging service property to reference the bean above -->
  <bean id="loggingBeforeAdvice"
class="net.klondike.aspects.loggingBeforeAdvice">
    <property name="loggingService">
      <ref bean="loggingService" />
    </property>
  </bean>

  <!—now define a NamedMethodPointcutAdvisor, set the advice property to the bean above, and set its mappedNames property to ‘*’ which will create a pointcut to match all methods excluding any init method -->
  <bean id="loggingAdvisor"
class="coldspring.aop.support.NamedMethodPointcutAdvisor">
    <property name="advice">
      <ref bean="loggingBeforeAdvice" />
    </property>
    <property name="mappedNames">
      <value>*</value>
    </property>
  </bean>

  <!-- to create the proxy object, first create our dao as the target object -->
  <bean id="catalogDaoTarget"
class="net.klondike.component.catalogDAO">
    <property name="dsn">
      <value>klondike</value>
    </property>
  </bean>

  <!-- now create a ProxyFactoryBean with the id catalogDAO, set the target to the catalogDaoTarget bean above, and give it ,the id of the NamedMethodPointcutAdvisor above in the list of interceptorNames -->
  <bean id="catalogDAO"
class="coldspring.aop.framework.ProxyFactoryBean">
    <property name="target">
      <ref bean="catalogDaoTarget" />
    </property>
    <property name="interceptorNames">
      <list>
        <value>loggingAdvisor</value>
      </list>
    </property>
  </bean>

  <!--now to use the proxy DAO, we give the id of the ProxyFactoryBean to the catalog service as the reference for the property catalogDAO -->
  <bean id="catalogService"
class="net.klondike.component.CatalogService">
    <property name="catalogDAO">
      <ref bean="catalogDAO" />
    </property>
  </bean>

</beans>


That’s it for the configuration. When you ask the ColdSpring bean factory for your catalogService, ColdSpring will create the ProxyFactoryBean and ask it to return a proxy object. The ProxyFactoryBean will in turn get a reference to the target object, the real DAO object, get the Advisor which contains the Aspect we created, causing the loggingService to be created, and will finally generate an entirely new cfc, which contains the target object, the proxy for the catalogDAO. This proxy object will be the same type as the target object and will contain all the same methods as the target, except that it will now flow every method through an instance of the loggingBeforeAdvice. If I want to add the same advice say to a gatewayObject, all I have to do is create another target object, and another ProxyFactoryBean and give it the same Advisor. If this doesn’t seem powerful enough for you, you can also create a new Advice, say a securityAdvice, and create a new Advisor, matching only the ‘save’ method, and give this to the ProxyBeanFactory we defined above. That would make that bean definition look something like this:

<bean id="catalogDAO"
class="coldspring.aop.framework.ProxyFactoryBean">
  <property name="target">
    <ref bean="catalogDaoTarget" />
  </property>
  <property name="interceptorNames">
    <list>
      <value>loggingAdvisor</value>
      <value>securityAdvisor</value>
    </list>
  </property>
</bean>


Adding the securityAdvisor after the loggingAdvisor causes it to be chained after the loggingAdvice, so if that Advice does something like throw an ‘InvalidCredentials’ error, the loggingAdvice will get a chance to write to the log before the method execution stops. So that’s just a small piece of the framework, the least powerful type of Advice we have available. After Advice is very similar to Before Advice, except that it occurs after method execution, giving you access to the return variable from the method as well as argument collection that was passed into the method. Wow, so that’s a pretty big blog post! I hope I’ve wet your appetite, and stay tuned because a super-prealpha-ubermethodology-oops-sorry-release is just around the corner!

3 Comments:

Anonymous Anonymous said...

Am I missing something, where did net.klondike.component.CatalogService come from? What is that cfc supposed to look like? Also you are doing a ref bean="loggingAdvice" when you named it loggingBeforeAdvice, or was this supposed to be different?

2:24 PM  
Anonymous Anonymous said...

Hi Chris

I'm finding the learning curve for this to be a little too steep for me. I'm grappling with the functionality and the examples and losing my grip unfortunately. If I have a service, and I want to add abstacted db connectivity, do I add a DAO object and link that back to the service via DAOtaget, and some sort of advisor? If so, i'm missing what goes on in the advisor because I can't seem to locate the one you talk about in your tutorial.

Would you have some examples that were more singular in their focus than this tutorial and a shade more involved than the documentation supplied?

Thanks

12:44 AM  
Anonymous Anonymous said...

ok, i finally get it. the DAOtarget allows me to create a proxy DAO object where I can then name point-cuts and an advisor. it wasn't until I found the petstore example and document that it all became much clearer. I have to say, i'm very impressed with the final result. totally worth the loss of hair.

Steve

11:40 PM  

Post a Comment

<< Home