Overcoming CFC Serialization Issues Using Java
*** THIS IS A REPOST OF AN EARLIER ARTICLE THAT GOT WIPED OUT ON A RECENT DB CRASH. ***
At work we always build our applications targeting clustered environments, that means that even if an app will be deployed to a single server at the beginning, it can always be moved to a cluster without any change. We also use our own infrastructure for clustering and don't rely on session replication or sticky sessions. That environment presents certain challenges for maintaining state within an application.
Our standard solution is to rely heavily on a DB backed Client scope. However, this has always limited how we can persist CFCs in the application. Typically when we want to persist a CFC we have to build our own serialization/desearialization mechanism to accomodate it to the client scope limitations. This solution would work fine for simple CFCs but it would rapidly become a hassle (to say the least) when dealing with composition/aggregation relationships.
Needless to say when I learned about the CFC Serialization feature in CF8 I was very eager to see how it could allow us to make better use of persistent CFCs. Sadly as soon as I started to make some simple proof of concepts I discovered (and later confirmed) the limitations of the current implementation of CFC Serialization in CF8. Apparently the serialization mechanism chokes when you have arrays, dates or query variables as instance variables in your CFC. Actually the problem is not the serialization, but the deserialization process.
Anyway, since other than that the CFC serialization worked beautifully (as far as I could tell), even with composition relationships, I still wanted to find a way to make use of this feature.
After a little bit of tinkering I found that I could make the CFC Serrialization work as intended if I just used a different Java class for the objects instead of the one provided implicitly by ColdFusion.
For example, instead of using ArrayNew(1) to declare my array, I found that using createObject("java","java.util.ArrayList").init() would work just fine. And for date objects using java.util.GregorianCalendar would do the trick.
Obviously the code that interacts with this Java objects need to be updated, but you can always create wrappers around it to handle the transformation between java.util.ArrayList and regular ColdFusion arrays, for example.
Well, here is the code for the object I used on my testing. I can now happily say that my days of limiting myself to persist only dull CFCs seem to be coming to an end :)
<cfset variables.instance = structNew()>
<cfset variables.instance.firstName = "">
<cfset variables.instance.lastName = "">
<cfset variables.instance.birthday = createObject("java","java.util.GregorianCalendar").init()>
<cfset variables.instance.relatives = structNew()>
<cffunction name="init" access="public" returntype="person" output="false">
<cfreturn this />
</cffunction>
<cffunction name="getName" access="public" returntype="string">
<cfreturn getFirstName() & " " & getLastName()>
</cffunction>
<cffunction name="getFirstName" access="public" returntype="string">
<cfreturn variables.instance.FirstName>
</cffunction>
<cffunction name="setFirstName" access="public" returntype="void">
<cfargument name="data" type="string" required="yes">
<cfset variables.instance.FirstName = arguments.data>
</cffunction>
<cffunction name="getLastName" access="public" returntype="string">
<cfreturn variables.instance.LastName>
</cffunction>
<cffunction name="setLastName" access="public" returntype="void">
<cfargument name="data" type="string" required="yes">
<cfset variables.instance.LastName = arguments.data>
</cffunction>
<cffunction name="getBirthday" access="public" returntype="date">
<cfset var d = variables.instance.Birthday>
<cfreturn createDate(d.get(d.YEAR), d.get(d.MONTH), d.get(d.DATE))>
</cffunction>
<cffunction name="setBirthday" access="public" returntype="void">
<cfargument name="data" type="date" required="yes">
<cfset variables.instance.Birthday = createObject("java","java.util.GregorianCalendar").init(year(arguments.data), month(arguments.data), day(arguments.data))>
</cffunction>
<cffunction name="addRelative" access="public" returntype="void">
<cfargument name="relation" type="string" required="yes">
<cfargument name="relative" type="person" required="yes">
<cfset var arr = 0>
<cfif not listFindNoCase(structKeyList(variables.instance.relatives),arguments.relation)>
<cfset variables.instance.relatives[arguments.relation] = createObject("java","java.util.ArrayList").init()>
</cfif>
<cfset arr = variables.instance.relatives[arguments.relation]>
<cfset arr.add(arguments.relative)>
</cffunction>
<cffunction name="getRelatives" access="public" returntype="any">
<cfargument name="relation" type="string" required="yes">
<cfset var arr = 0>
<cfset var i = 0>
<cfset var rtn = arrayNew(1)>
<cfif not listFindNoCase(structKeyList(variables.instance.relatives),arguments.relation)>
<cfthrow message="Invalid relation type. Valid types are: #structKeyList(variables.instance.relatives)#">
</cfif>
<cfset arr = variables.instance.relatives[arguments.relation]>
<cfloop from="0" to="#arr.size()-1#" index="i">
<cfset arrayAppend(rtn, arr.get(i))>
</cfloop>
<cfreturn rtn>
</cffunction>
<cffunction name="getRelativeTypes" access="public" returntype="string">
<cfreturn structKeyList(variables.instance.relatives)>
</cffunction>
</Cfcomponent>
PS: You can find the code for serializing/deserializing CFCs here and here.

One thought - I've noticed that a de-serialised object causes null pointer errors when it is returned from a function with a fixed returntype, e.g.
<cffunction name="getBean" returntype="model.beans.myBean">
<cfset var myBean = "">
<cfset myBean = createObject("component","model.beans.myBean").init()>
<cfreturn myBean
</cffunction>
will work fine, whereas...
<cffunction name="getDeserialisedBean" returntype="model.beans.myBean">
<cfargument name="serialisedBean">
<cfset var myBean = "">
<cfset myBean = deSerialise(arguments.serialisedBean)>
<cfreturn myBean
</cffunction>
... will return a null pointer error. Note it is NOT during the deSerialisation that the error occurs (as it would if the bean contained a cf array or query etc...) but actually during the return. If I change the function to:
<cffunction name="getDeserialisedBean" returntype="any">
<cfargument name="serialisedBean">
<cfset var myBean = "">
<cfset myBean = deSerialise(arguments.serialisedBean)>
<cfreturn myBean
</cffunction>
... the problem goes away. Now, it's easy to fix obviously, but nevertheless type checking is desirable.
Is this your experience?
Thats very interesting, I haven't tried type-checking a deserialized bean before, but given the issues with the current serialization implementation in CF8 I am not surprised of any "quirks" you may find. Let's hope this feature is more polished on CF9.