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.
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"?
ReplyDeleteWhile 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.
ReplyDeleteInteresting. 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.
ReplyDeleteI 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.
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.
ReplyDeleteProbably 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.