Unlocking the power of CF Metadata
Lately I have been thinking an awful lot about metadata. We have this great metadata object in CF, but I rarely hear anything about it. Funny, even though we use metadata extensively in ColdSpring, I sort of forgot it was there for day to day use. ColdFusion's metadata object is free for the use in almost any way we want, and can provide extremely powerful class or method level annotations. Let's take a quick look at an example so you can see what I'm getting at. Write yourself any cfc, and write a simple cfm page to dump it's metadata:
This will output a big struct with all sorts of information about your object, like its name, extends, path, and lots of information its functions, like name, access, etc. There's a whole lot of great information for you to use, but what's really cool is, you can put your own stuff in there. Just make sure you don't try to overwrite any attributes that ColdFusion uses. This can be easily accomplished by using a prefix to create your own namespace. For my examples we'll use 'cs_' for our attributes. So lets add some custom metadata to our cfc. In the cfcomponent tag, lets add an attribute 'cs_metadata="really cool stuff"' (<cfcomponent name="mycfc" cs_metadata="really cool stuff"...) and run the cfm page we made in the previous example. Check out the output, now we have a cs_metadata key with the value "really cool stuff". Really cool, huh? Or are you thinking, so what? The thing that got me all excited about this is, if we use annotations on methods in our cfcs, we can access them in Advice components (you knew I was gonna get to some AOP right?), which is in fact really really cool stuff.
The great thing about AOP is it gives you the ability to develop abstract systems in an extremely generic manner. But when we apply them to specific components, we want them to take on more concrete functionality. The easiest way to do this is to provide your Aspects with parameters during configuration, enabling them to become more specialized to your components. For example, in the Klondike record store example I have been showing in my sessions, I have a generic caching system which can be configured for each type of component to cache by providing information to the Aspects. But the downfall of this is, I have to configure that Aspect over and over in ColdSpring for each model component. Enter metadata!
Let's take a look at a super simple system, one that always show up in any AOP conversation, logging. It's pretty easy to write a very general logging system. I like to wrap the logging in a service so we can reuse it anywhere, and provide log level methods, info(message), warn(message), error(message), debug(message), this way I can swap out cflog for log4j and control the log level in one place. Now all we need is an Aspect that will build a log statement from a method call. What if we would like more control, say to only include certain parameters for certain methods. This would be a nightmare if we tried to provide some sort of configuration to the Aspect, but we can easily put this info in the function metadata and read in in the Aspect! Here's how it's done, first we'll add some metadata to a cfc that we want to log method calls, notice I have added a cs_LogFields attribute:
Next we have our logging service, I've used cflog in this example and added a setter for the log file name.
So now we will need an Advice class to tie it all together. I am going to use an AfterReturningAdvice for this one, it's going to be a bit more complex, so I'll put some comments in line.
If you've followed any of my previous AOP posts, you should have no problem setting all this up in your ColdSpring config file. I'll also include those links. This may look a bit complex at first sight, but look what we are gaining now. I could have provided setters to my advice class, allowing the application developer to configure it to specific uses, but that would mean they would need to configure this class over and over again for each different use, as I did for my caching system. By creating my advice class to read metadata, though, I have still provided that same customization, but this time with minimal work for the developers. Plus, we have provided the power of configuration right where the developer is working, in the cfcs themselves. This may not be the best way to go for all systems by any means, I still feel that the real power of ColdSpring AOP is its declarative approach, and it can be argued that this is going in the opposite direction. But the sheer power of this approach can open many doors to designing truly generic systems. For me that is the ultimate goal of AOP.
Happy programming, and I hope to see some readers at the frameworks conference where I plan to get a lot more in-depth into these concepts both in my session and drinking in the bar! See you!
Introduction to ColdSpring AOP
ColdSpring AOP Tutorial – Part One
ColdSpring AOP Tutorial – Part Two, Around Advice
Introducing auto-magic Remoting with ColdSpring
<cfset obj = CreateObject('component','com.foo.mycfc').init() />
<cfdump var="#getMetadata(obj)#" />
<cfdump var="#getMetadata(obj)#" />
This will output a big struct with all sorts of information about your object, like its name, extends, path, and lots of information its functions, like name, access, etc. There's a whole lot of great information for you to use, but what's really cool is, you can put your own stuff in there. Just make sure you don't try to overwrite any attributes that ColdFusion uses. This can be easily accomplished by using a prefix to create your own namespace. For my examples we'll use 'cs_' for our attributes. So lets add some custom metadata to our cfc. In the cfcomponent tag, lets add an attribute 'cs_metadata="really cool stuff"' (<cfcomponent name="mycfc" cs_metadata="really cool stuff"...) and run the cfm page we made in the previous example. Check out the output, now we have a cs_metadata key with the value "really cool stuff". Really cool, huh? Or are you thinking, so what? The thing that got me all excited about this is, if we use annotations on methods in our cfcs, we can access them in Advice components (you knew I was gonna get to some AOP right?), which is in fact really really cool stuff.
The great thing about AOP is it gives you the ability to develop abstract systems in an extremely generic manner. But when we apply them to specific components, we want them to take on more concrete functionality. The easiest way to do this is to provide your Aspects with parameters during configuration, enabling them to become more specialized to your components. For example, in the Klondike record store example I have been showing in my sessions, I have a generic caching system which can be configured for each type of component to cache by providing information to the Aspects. But the downfall of this is, I have to configure that Aspect over and over in ColdSpring for each model component. Enter metadata!
Let's take a look at a super simple system, one that always show up in any AOP conversation, logging. It's pretty easy to write a very general logging system. I like to wrap the logging in a service so we can reuse it anywhere, and provide log level methods, info(message), warn(message), error(message), debug(message), this way I can swap out cflog for log4j and control the log level in one place. Now all we need is an Aspect that will build a log statement from a method call. What if we would like more control, say to only include certain parameters for certain methods. This would be a nightmare if we tried to provide some sort of configuration to the Aspect, but we can easily put this info in the function metadata and read in in the Aspect! Here's how it's done, first we'll add some metadata to a cfc that we want to log method calls, notice I have added a cs_LogFields attribute:
<cfcomponent displayname="EntryService">
... init method and setters for dependencies ...
<cffunction name="getEntriesByCategoryID" access="public" returntype="array" cs_LogFields="categoryID" output="false" >
<cfargument name="categoryID" type="numeric" required="true" />
<cfargument name="numToReturn" type="numeric" required="false" default="-1" />
<cfargument name="activeOnly" type="boolean" required="false" default="true" />
<cfreturn variables.entryGateway.getEntries(arguments.numToReturn, arguments.activeOnly, arguments.categoryID) />
</cffunction>
... more service methods ...
</cfcomponent>
... init method and setters for dependencies ...
<cffunction name="getEntriesByCategoryID" access="public" returntype="array" cs_LogFields="categoryID" output="false" >
<cfargument name="categoryID" type="numeric" required="true" />
<cfargument name="numToReturn" type="numeric" required="false" default="-1" />
<cfargument name="activeOnly" type="boolean" required="false" default="true" />
<cfreturn variables.entryGateway.getEntries(arguments.numToReturn, arguments.activeOnly, arguments.categoryID) />
</cffunction>
... more service methods ...
</cfcomponent>
Next we have our logging service, I've used cflog in this example and added a setter for the log file name.
<cfcomponent displayname="LoggingService">
<cffunction name="init" returntype="net.litepost.component.logging.LoggingService" access="public" output="false">
<cfreturn this />
</cffunction>
<cffunction name="setFilename" access="public" returntype="void" output="false">
<cfargument name="filename" type="string" required="true" />
<cfset variables.filename = arguments.filename />
</cffunction>
<cffunction name="info" access="public" returntype="void" output="false">
<cfargument name="message" type="string" required="true" />
<cflog file="#variables.filename#" type="information" text="#arguments.message#">
</cffunction>
... rest of logging methods ...
</cfcomponent>
<cffunction name="init" returntype="net.litepost.component.logging.LoggingService" access="public" output="false">
<cfreturn this />
</cffunction>
<cffunction name="setFilename" access="public" returntype="void" output="false">
<cfargument name="filename" type="string" required="true" />
<cfset variables.filename = arguments.filename />
</cffunction>
<cffunction name="info" access="public" returntype="void" output="false">
<cfargument name="message" type="string" required="true" />
<cflog file="#variables.filename#" type="information" text="#arguments.message#">
</cffunction>
... rest of logging methods ...
</cfcomponent>
So now we will need an Advice class to tie it all together. I am going to use an AfterReturningAdvice for this one, it's going to be a bit more complex, so I'll put some comments in line.
<cfcomponent name="loggingAdvice" extends="coldspring.aop.AfterReturningAdvice">
<!--- setters for the logging service --->
<cffunction name="setLoggingService" returntype="void" access="public" output="false"
hint="Dependency: security service">
<cfargument name="loggingService" type="net.litepost.service.LoggingService" required="true"/>
<cfset variables.m_loggingService = arguments.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 arg = '' />
<cfset var argString = '' />
<cfset var logFields = "" />
<!--- first I need the metadata for the target object of the method call --->
<cfset var objMd = getMetadata(arguments.target) />
<cfset var objName = objMd.name />
<!--- I also need the name of the method that was called --->
<cfset var methodName = method.getMethodName() />
<!--- I will use a private function to retrieve the metadata for that function --->
<cfset var functionMd = getFunctionMetadata(objMd,methodName) />
<!--- We use StructKeyExist to find our custom attribute, which we use as the logFields. If the attribute is missing, we are going to assume we want to log all the arguments to the method call --->
<cfif StructKeyExists(functionMd, "cs_LogFields")>
<cfset logFields = functionMd.cs_LogFields>
<cfelse>
<cfset logFields = StructKeyList(args)/>
</cfif>
<!--- now all we need to do is loop over our logFields list, and build out a string from those values (restricting to simpleValues) --->
<cfloop list="#logFields#" index="arg">
<cfif StructKeyExists(args, arg) and isSimpleValue(args[arg])>
<cfif len(argString)>
<cfset argString = argString & ', ' />
</cfif>
<cfset argString = argString & arg & '=' & args[arg] >
</cfif>
</cfloop>
<!--- Now send it to the logging service! --->
<cfset variables.m_loggingService.info("[" & objName & "] " & method.getMethodName() & "(" & argString & ") called!") />
</cffunction>
<!--- here's our private method to retrieve just a particular function metadata from the full metadata from a cfc --->
<cffunction name="getFunctionMetadata" access="private" returntype="struct" output="false">
<cfargument name="metadata" type="struct" required="true"/>
<cfargument name="function" type="string" required="true"/>
<cfset var ix = 0/>
<cfset var mdFunctions = arguments.metadata.functions/>
<cfloop from="1" to="#ArrayLen(mdFunctions)#" index="ix">
<cfif mdFunctions[ix].name EQ arguments.function>
<cfreturn mdFunctions[ix]/>
</cfif>
</cfloop>
<cfreturn StructNew()/>
</cffunction>
</cfcomponent>
<!--- setters for the logging service --->
<cffunction name="setLoggingService" returntype="void" access="public" output="false"
hint="Dependency: security service">
<cfargument name="loggingService" type="net.litepost.service.LoggingService" required="true"/>
<cfset variables.m_loggingService = arguments.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 arg = '' />
<cfset var argString = '' />
<cfset var logFields = "" />
<!--- first I need the metadata for the target object of the method call --->
<cfset var objMd = getMetadata(arguments.target) />
<cfset var objName = objMd.name />
<!--- I also need the name of the method that was called --->
<cfset var methodName = method.getMethodName() />
<!--- I will use a private function to retrieve the metadata for that function --->
<cfset var functionMd = getFunctionMetadata(objMd,methodName) />
<!--- We use StructKeyExist to find our custom attribute, which we use as the logFields. If the attribute is missing, we are going to assume we want to log all the arguments to the method call --->
<cfif StructKeyExists(functionMd, "cs_LogFields")>
<cfset logFields = functionMd.cs_LogFields>
<cfelse>
<cfset logFields = StructKeyList(args)/>
</cfif>
<!--- now all we need to do is loop over our logFields list, and build out a string from those values (restricting to simpleValues) --->
<cfloop list="#logFields#" index="arg">
<cfif StructKeyExists(args, arg) and isSimpleValue(args[arg])>
<cfif len(argString)>
<cfset argString = argString & ', ' />
</cfif>
<cfset argString = argString & arg & '=' & args[arg] >
</cfif>
</cfloop>
<!--- Now send it to the logging service! --->
<cfset variables.m_loggingService.info("[" & objName & "] " & method.getMethodName() & "(" & argString & ") called!") />
</cffunction>
<!--- here's our private method to retrieve just a particular function metadata from the full metadata from a cfc --->
<cffunction name="getFunctionMetadata" access="private" returntype="struct" output="false">
<cfargument name="metadata" type="struct" required="true"/>
<cfargument name="function" type="string" required="true"/>
<cfset var ix = 0/>
<cfset var mdFunctions = arguments.metadata.functions/>
<cfloop from="1" to="#ArrayLen(mdFunctions)#" index="ix">
<cfif mdFunctions[ix].name EQ arguments.function>
<cfreturn mdFunctions[ix]/>
</cfif>
</cfloop>
<cfreturn StructNew()/>
</cffunction>
</cfcomponent>
If you've followed any of my previous AOP posts, you should have no problem setting all this up in your ColdSpring config file. I'll also include those links. This may look a bit complex at first sight, but look what we are gaining now. I could have provided setters to my advice class, allowing the application developer to configure it to specific uses, but that would mean they would need to configure this class over and over again for each different use, as I did for my caching system. By creating my advice class to read metadata, though, I have still provided that same customization, but this time with minimal work for the developers. Plus, we have provided the power of configuration right where the developer is working, in the cfcs themselves. This may not be the best way to go for all systems by any means, I still feel that the real power of ColdSpring AOP is its declarative approach, and it can be argued that this is going in the opposite direction. But the sheer power of this approach can open many doors to designing truly generic systems. For me that is the ultimate goal of AOP.
Happy programming, and I hope to see some readers at the frameworks conference where I plan to get a lot more in-depth into these concepts both in my session and drinking in the bar! See you!
Introduction to ColdSpring AOP
ColdSpring AOP Tutorial – Part One
ColdSpring AOP Tutorial – Part Two, Around Advice
Introducing auto-magic Remoting with ColdSpring