Tuesday, September 1, 2009

Annotation-based Dependency Injection using ColdSpring

ColdSpring is by far the most essential tool I need for building ColdFusion applications. However, I'm not a huge fan of writing a ton of XML and I'm certainly not a fan of writing a lot of getters and setters inside my components.

To help ease that pain, I borrowed a feature from Model-Glue 3 and extended ColdSpring to create the "beans" scope.

Little did I realize ColdSpring can already handle a lot of what I wanted to do without having to modify ColdSpring itself simply by creating a factory post processor. To automatically inject the "beans" scope into my ColdSpring-managed beans, I created the following BeansScopeFactoryPostProcessor.cfc:


<cfcomponent>

<cffunction name="postProcessBeanFactory" access="public" returntype="void">

<cfset var local = {} />

<cfset local.beanDefs = getConcreteBeanClasses() />

<cfloop collection="#local.beanDefs#" item="local.beanName">

<cfset local.beanList = getBeanScopeList(local.beanDefs[local.beanName]) />

<cfif local.beanList neq "">

<cfset local.bean = getBeanFactory().getBean(local.beanName) />

<cfset injectBeansScope(local.bean,local.beanList) />

</cfif>

</cfloop>

</cffunction>

<cffunction name="getConcreteBeanClasses" access="private" returntype="struct">

<cfset var local = {} />

<cfset local.classes = {} />

<cfset local.beanDefs = getBeanFactory().getBeanDefinitionList() />

<cfloop collection="#local.beanDefs#" item="local.beanName">

<cfif not local.beanDefs[local.beanName].isAbstract()>
<cfset local.classes[local.beanName] = local.beanDefs[local.beanName].getBeanClass() />
</cfif>

</cfloop>

<cfreturn local.classes />

</cffunction>

<cffunction name="injectBeansScope" access="private" returntype="void">
<cfargument name="bean" required="true" />
<cfargument name="beanList" required="true" />

<cfset var local = {} />

<cfif isCFC(arguments.bean)>

<cfset local.beans = {} />

<cfloop list="#arguments.beanList#" index="local.beanName">
<cfset local.beans[local.beanName] = getBeanFactory().getBean(local.beanName) />
</cfloop>

<cfset arguments.bean.__setVariable = variables.__setVariable />

<cfset arguments.bean.__setVariable("beans",local.beans) />

<cfset StructDelete(arguments.bean,"__setVariable") />

</cfif>

</cffunction>

<cffunction name="getBeanScopeList" access="private" returntype="string">
<cfargument name="bean" required="true" />

<cfset var local = {} />

<cfset local.metaData = getComponentMetaData(arguments.bean) />

<cfset local.beanList = "" />

<cfif StructKeyExists(local.metaData,"beans")>

<cfloop list="#local.metaData.beans#" index="local.beanName">
<cfset local.beanList = ListAppend(local.beanList,local.beanName) />
</cfloop>

</cfif>

<cfset local.extendedMetaData = local.metaData />

<cfloop condition="StructKeyExists(local.extendedMetaData,'extends')">

<cfif StructKeyExists(local.extendedMetaData,"beans")>

<cfloop list="#local.extendedMetaData.beans#" index="local.beanName">

<cfif not listFindNoCase(local.beanList,local.beanName)>
<cfset local.beanList = ListAppend(local.beanList,local.beanName) />
</cfif>

</cfloop>

</cfif>

<cfset local.extendedMetaData = local.extendedMetaData.extends />

</cfloop>

<cfreturn local.beanList />

</cffunction>

<cffunction name="setBeanFactory" access="public" returntype="void">
<cfargument name="beanFactory" required="true" type="coldspring.beans.BeanFactory" />

<cfset variables.beanFactory = arguments.beanFactory />

</cffunction>

<cffunction name="getBeanFactory" access="private" returntype="any">

<cfreturn variables.beanFactory />

</cffunction>

<cffunction name="__setVariable" access="public" returntype="void">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

<cfset variables[arguments.key] = arguments.value />

</cffunction>

<cffunction name="isCFC" access="private" returntype="boolean">
<cfargument name="object" required="true" />

<cfset var metaData = getMetaData(arguments.object) />

<cfreturn isObject(arguments.object) and structKeyExists(metaData,"type") and metaData.type eq "component" />

</cffunction>

</cfcomponent>


Long story short, it looks at all the beans defined in ColdSpring, checks their metadata for a "beans" attribute inside the cfcomponent tag, and automatically injects the requested beans into the component inside a variables.beans struct.

To get ColdSpring to process my beans, I define the factory post processor as such:


<bean id="beanInjector" class="test.BeansScopeFactoryPostProcessor" factory-post-processor="true" />


For those unaware of how factory post-processors work, ColdSpring will look for any beans where factory-post-processor="true" and automatically call postProcessBeanFactory() on those beans once the bean factory has been initialized.

To keep the code relatively small in this post, I removed any comments. Hopefully it's still somewhat straight-forward and easy to follow.

4 comments:

  1. I have always had a tough time reading XML for some reason. It's the custom tags that throw me off. This is pretty cool. Did you mean to have the space for the function name " setVariable"?

    ReplyDelete
  2. While you could name the injection method anything, I chose "__setVariable" because I didn't want to override any methods that already exist inside my CFCs and I took the assumption that people wouldn't already have a method named __setVariable.

    ReplyDelete
  3. Interesting. What I'm doing in CF9 is using a cfproperty tag to create an implicit setter for each bean that I need. I then use Brian Kotek's BeanInjector to automatically inject any dependencies, which can be done with a single line of code.

    I think it accomplishes the same thing that you're doing, but it does require one property per bean, as opposed to your list of beans, which is more succinct.

    ReplyDelete
  4. I don't mind autowiring dependencies using cfproperty tags (I think that's how ColdBox does it too), I just found out about the "beans" attribute from Model-Glue and it seemed to make sense to me.

    Probably the biggest downside to using a factory post processor is that ColdSpring needs to construct all the beans that have dependencies on initialization, which means no more lazy-loading. However, if ColdSpring (fully) supported bean post processors, then lazy-loading would work fine. Hopefully it'll be fully supported in the next release of ColdSpring, which I heard is in the works. In the meantime, I extended ColdSpring to add my own bean post processor support.

    ReplyDelete