I am a big fan of ColdSpring. I have been for quite a while. I am also aware of the fact that I typically use only a small portion of what ColdSpring offers us. I use it mostly for bean definitions and managing dependencies. I always wanted to play around with the AOP (aspect oriented programming) pieces of ColdSpring, but I never had a real need to do so…or so I thought.
On a recent project, we had the need to have our model return data in a slightly different format when being called from a remoting proxy (also created by ColdSpring). In this case we needed to return data for use in a . Initially, we simple added a method that was solely used for AJAX calls. This can cause issues as now we had very similar code, doing a very similar process in more than once place. This can lead to issues if there were changes made to one method and not the other. The solution we came up with was to use method interception in ColdSpring.Basically, method interception, is a way that we can use ColdSpring to process method calls differently by ‘intercepting’ them.
It may seem difficult to comprehend where this would be needed, but I think the above example is a great one. Lets say you have a service object named WidgetService and inside that object is a method named getWidgets(). In our original code, we might also have a method named getWidgetsFromAJAX() that would format the data for use in the . Both of these methods would need to accept the same arguments, query the database the same way and return data. The only difference is that in the second method, we would need to call QueryConvertForGrid() before returning the results. This just feels dirty to me.
Using method interception, we can tell ColdSpring that if the method call comes from a certain file (like our remoting proxy), to handle it differently, and in this case, format the data to be used with . Lets start by going over the ColdSpring configuration.
The following is a simple bean definition for our widget service.
<bean id="WidgetService" class="com.alagad.demo.WidgetService" />
The following is our intercept definition. The interceptor names are other beans, and in this case the bean name is the bean that will be used during our intercept. This bean is also used as our ‘target’ for the remoting proxy. This is how we limit the intercept to only calls made through the remoting proxy.
<bean id="ajaxWidgetService" class="coldspring.aop.framework.ProxyFactoryBean"> <property name="target"> <ref bean="WidgetService" /> </property> <property name="interceptorNames"> <list> <value>ajaxMethodInterceptorAdvice</value> </list> </property> </bean>
The following will create our remoting proxy. This definition tells ColdSpring to create a file named ‘RemoteWidgetService.cfc’ that is based off of the ‘ajaxWidgetService’ bean and will include the method named ‘getWidgets’ in the remoting proxy.
<bean id="remoteWidgetService" class="coldspring.aop.framework.RemoteFactoryBean" parent="abstractRemoteProxy" lazy-init="true"> <property name="target"> <ref bean="ajaxWidgetService"/> </property> <property name="serviceName"> <value>RemoteWidgetService</value> </property> <property name="remoteMethodNames"> <value>getWidgets</value> </property> </bean>
The following is the simple bean definition for our intercept component. ColdSpring will use the code in this component during the intercept.
<bean id="ajaxMethodInterceptorAdvice" class="com.alagad.demo.ajaxMethodInterceptorAdvice" />
Now lets create a simple WidgetService.cfc that will return a query object from the getWidgets() method.
<cfcomponent ><br /> <cffunction name="getWidgets" returntype="query" output="false" access="public"> <cfset var widgets = queryNew("id,name") /> <cfset var i = 0 /> <cfloop from="1" to="50" index="i"> <cfset queryAddRow(widgets) /> <cfset querySetCell(widgets, "id", i) /> <cfset querySetCell(widgets, "name", "widget"&i) /> </cfloop> <cfreturn widgets /> </cffunction> </cfcomponent>
When ColdSpring creates our remoting proxy based off of this object, it will look something like this:
<cfcomponent name="RemoteAdministrationService" displayname="RemoteAdministrationService:RemoteProxyBean" hint="Abstract Base Class for Aop Based Remote Proxy Beans" output="false"> <cfset variables.proxyId = CreateUUId() /> <cfset variables.beanFactoryName = "cs" /> <cfset variables.beanFactoryScope = "application" /> <cfset variables.constructed = false /> <cfset setup() /> <cffunction name="setup" access="public" returntype="void"> <cfset var bfUtils = 0 /> <cfset var bf = 0 /> <cfset var error = false /> <!--- I want to make sure that the proxy id really exists ---> <cfif not StructKeyExists(variables, "proxyId")> <cfset variables.proxyId = CreateUUId() /> </cfif> <cflock name="RemoteProxyBean.#variables.proxyId#.Setup" type="readonly" timeout="5"> <cfif not StructKeyExists(variables, "constructed") or not variables.constructed> <!--- it looks like there is an issue with setting up the variables scope in a static initializer with remote methods, so we will make sure things are set up ---> <cfif not StructKeyExists(variables, "constructed")> <cfset variables.beanFactoryName = "cs" /> <cfset variables.beanFactoryScope = "application" /> <cfset variables.constructed = false /> </cfif> <!--- make sure scope is setup (could have been set to ', meaning application, default) ---> <cfif not len(variables.beanFactoryScope)> <cfset variables.beanFactoryScope = 'application' /> </cfif> <cftry> <cfset bfUtils = createObject("component","coldspring.beans.util.BeanFactoryUtils").init()/> <cfif not len(variables.beanFactoryName)> <cfset bf = bfUtils.getDefaultFactory(variables.beanFactoryScope) /> <cfelse> <cfset bf = bfUtils.getNamedFactory(variables.beanFactoryScope, variables.beanFactoryName) /> </cfif> <cfset remoteFactory = bf.getBean("&RemoteAdministrationService") /> <cfset variables.target = bf.getBean("RemoteAdministrationService") /> <cfset variables.adviceChains = remoteFactory.getProxyAdviceChains() /> <cfset variables.constructed = true /> <cfcatch> <cfset error = true /> </cfcatch> </cftry> </cfif> </cflock> <cfif error> <cfthrow type="coldspring.remoting.ApplicationContextError" message="Sorry, a ColdSpring BeanFactory named #variables.beanFactoryName# was not found in #variables.beanFactoryScope# scope. Please make sure your bean factory is properly loaded. Perhapse your main application is not running?" /> </cfif> </cffunction> <cffunction name="callMethod" access="private" returntype="any"> <cfargument name="methodName" type="string" required="true" /> <cfargument name="args" type="struct" required="true" /> <cfset var adviceChain = 0 /> <cfset var methodInvocation = 0 /> <cfset var rtn = 0 /> <cfset var method = 0 /> <!--- make sure setup is called ---> <cfif not StructKeyExists(variables, "constructed") or not variables.constructed> <cfset setup() /> </cfif> <!--- if an advice chain was created for this method, retrieve a methodInvocation chain from it and proceed ---> <cfif StructKeyExists(variables.adviceChains, arguments.methodName)> <cfset method = CreateObject('component','coldspring.aop.Method').init(variables.target, arguments.methodName, arguments.args) /> <cfset adviceChain = variables.adviceChains[arguments.methodName] /> <cfset methodInvocation = adviceChain.getMethodInvocation(method, arguments.args, variables.target) /> <cfreturn methodInvocation.proceed() /> <cfelse> <!--- if there's no advice chains to execute, just call the method ---> <cfinvoke component="#variables.target#" method="#arguments.methodName#" argumentcollection="#arguments.args#" returnvariable="rtn"> </cfinvoke> <cfif isDefined('rtn')> <cfreturn rtn /> </cfif> </cfif> </cffunction> <cffunction name="getWidgets" access="remote" returntype="any" output="false" > <cfset var rtn = callMethod('getWidgets', arguments) /> <cfif isDefined('rtn')><cfreturn rtn /></cfif> </cffunction> </cfcomponent>
Now whenever you use the remoting proxy, ColdSpring handles making the call to the WidgetService in your model without needing any other code.
Lastly, we need to create our intercept CFC:
<cfcomponent extends="coldspring.aop.MethodInterceptor"> <cffunction name="init" access="private" returntype="void" output="false"> </cffunction> <cffunction name="invokeMethod" access="public" returntype="any"> <cfargument name="methodInvocation" type="coldspring.aop.MethodInvocation" required="true" /> <cfset var result = 0 /> <cfset var args = arguments.methodInvocation.getArguments() /> <cfset var arg = 0 /> <cfinvoke component="#arguments.methodInvocation.getTarget()#" method="#arguments.methodInvocation.getMethod().getMethodName()#" returnvariable="result"> <cfloop collection="#args#" item="arg"> <cfif StructKeyExists(args, arg)> <cfinvokeargument name="#arg#" value="#args[arg]#" /> </cfif> </cfloop> </cfinvoke> <cfif IsDefined("result") AND IsQuery(result)> <cfif StructKeyExists(args, "page") AND StructKeyExists(args, "pageSize")> <cfset result = queryConvertForGrid(result, args.page, args.pageSize ) /> </cfif> </cfif> <cfreturn result /> </cffunction> </cfcomponent>
This code is what actually gets run during the intercept. What we do here is use to make the method call and pass along any arguments using . Then, as long as the result of that method call is a ColdFusion query object, we call the queryConvertForGrid() method and return that value. In our project, we had a lot more code as we needed to massage some of the values returned before shipping it off to .
This example uses a bit of ColdSpring’s functionality, so lets break it down one more time.
First, we have a simple service, named ‘widgetService’ that has a method named ‘getWidgets’. This method returns a ColdFusion query object containing the id and name of the widget.
Next we have a bean named ‘ajaxWidgetService’ that defines our intercept and tells ColdSpring to use the ‘ajaxMethodInterceptorAdvice’ bean as an interceptor.
Then we create our remoting proxy by telling ColdSpring what bean to base the proxy on.
Lastly, we have the bean that will be used for our intercept.
Now lets follow the path of execution. We make a remote call to the ‘getWidgets’ method on the ‘remoteWidgetService’. This in turn calls the ‘getWidgets’ method on the widgetService, however, ColdSpring intercepts this method call, and instead, calls the ‘invokeMethod’ method on our interceptor. This method then makes the call to the ‘target’, which in this case is the ‘widgetService’. If the result of this method call is a ColdFusion query, we run call the queryConvertForGrid() method and finally return that result.