Monday, March 7, 2011

Turning Spring MVC + Hibernate lazy=true causes ClassCastException

I started getting java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
 after I had changed the default fetching strategy to be lazy to give me better loading performance.

<hibernate-mapping auto-import="true" default-lazy="true" default-cascade="save-update">

This worked to improve performance, but what I couldn't understand was why I was getting the dreaded ClassCastException. To understand why, I had to dig deeper into Hibernate and Spring MVC and learn more about what was happening underneath the hood.

Here is the shortened class definition of the entity:

public class Entity {
    
    private transient Set<Long> selectedMice = new HashSet<Long>();

What's important to note is that the field called "selectedMice" is not mapped in Hibernate to be persisted to the database. Rather this field is used to temporarily store data to be processed and hence it is labelled as being "transient".

Now when combined with the Spring MVC framework, binding occurs where  form submission values destined for the selectedMice field are converted into their appropriate value of Long.


                                    <spring:bind path="model.selectedMice">
                                        <input type="checkbox" name="${status.expression}" value="${mouse.id}" />                                        
                                    </spring:bind>

When lazy loading is turned on, the Entity class is subclassed by Hibernate using CGLIB to create a proxy. The problem is that CGLIB does not recognize Generics. So when CGLIB goes to create the proxy class, rather than keeping the parameterized type of Set, it simply becomes Set.

Since Spring MVC is now populating a proxy class instead of the direct Entity class, it doesn't know the parameterized type of Set collection for selectedMice field, and does no automatic conversion of String to Long. The selectedMice field becomes populated with Strings instead of Longs. Thus when I go to fetch the selectedMice and cast it to Long, I get the ClassCastException.

Solution

The solution is quite simple and is probably better for clearer separation of application layers. I've moved all transient fields out of the entity and into a model class as follows:

public class Model {
    /**
     * A proxied instance generated by Hibernate/CGLIB
     */
    private Entity entity;

    /**
     * The non-persistent field
     */
    private transient Set<Long> selectedMice = new HashSet<Long>();


The Model class wraps around the Entity class (which is proxied) and also contains the selectedMice field. This way Spring MVC binds to the Model class rather than to the proxy.

References:

http://blog.anthonychaves.net/2007/03/12/howd-this-string-get-into-my-list/
http://andykayley.blogspot.com/2009/05/how-to-avoid-classcastexceptions-when.html
http://download.oracle.com/javase/tutorial/extra/generics/intro.html

No comments:

Post a Comment