Friday 6 November 2009

JAX-WS + Hibernate = LazyInitializationException???

Using JAX-WS and Hibernate together could easily lead to the LazyInitializationException when JAXB tries to serialize a property that is a Hibernate proxy. Open Session in View pattern may be an answer to the problem but in that case, the whole object graph will also be loaded and thus, cause big performance problem. Manually discarding the proxied objects is also an option but it requires a lot of code. So, loading on demand only a part of the object graph with JAX-WS and Hibernate seems to be not a trivial problem. Fortunately, it could be solved by a rather simple idea: Creating JAXB XML accessor to wrap around every lazy properties and return a NULL value whenever the accessed properties have not been initialized by Hibernate. Here are the steps:
  1. Specifying which part of the object graph will be loaded by Hibernate thank to the DataExtent feature in EL4J: http://philhoser.blogspot.com/2008/11/dataextends-to-controll-lazy-loading.html
  2. Extending com.sun.xml.bind.v2.runtime.reflect.Accessor to create an XML accessor that will intercept before a property is accessed and return NULL if the target entity has not been initialized
    /**
    * <p>
    * A customized XML accessor for getting rid of the {@link LazyInitializationException} when JAXB
    * serializes a Hibernate proxied entity.
    * </p>
    * 
    */
    public class HibernateProxiedToNullXmlAccessor<BeanT, ValueT> extends
    com.sun.xml.bind.v2.runtime.reflect.Accessor<BeanT, ValueT> {
    
    private Field f;
    
    /**
    * <p>
    * The constructor.
    * </p>
    * 
    * @param valueType
    */
    protected HibernateProxiedToNullXmlAccessor(Class<ValueT> valueType, Field f) {
    super(valueType);
    this.f = f;
    }
    
    /**
    * @inheritdoc
    */
    @Override
    public ValueT get(BeanT bean) throws AccessorException {
    ValueT result = null;
    PropertyUtilsBean utils = new PropertyUtilsBean();
    try {
    if (Hibernate.isInitialized(bean)) {
    Object property = utils.getProperty(bean, f.getName());
    if (Hibernate.isInitialized(property)) {
    result = (ValueT) property;
    }
    }
    } catch (IllegalAccessException e) {
    throw new AccessorException(e);
    } catch (InvocationTargetException e) {
    throw new AccessorException(e);
    } catch (NoSuchMethodException e) {
    throw new AccessorException(e);
    }
    return result;
    }
    
    /**
    * @inheritdoc
    */
    @Override
    public void set(BeanT arg0, ValueT arg1) throws AccessorException {
    // Do nothing.
    }
    }
    
    

  3. Implementing com.sun.xml.bind.AccessorFactory to use the previously created Accessor on lazy properties
    public class XmlAccessorFactoryImpl implements AccessorFactory {
    
    /** 
    * @inheritdoc
    */
    public Accessor createFieldAccessor(Class bean, Field f, boolean readOnly) throws JAXBException {
    Annotation[] annotations = f.getAnnotations();
    
    for (Annotation ann : annotations) {
    if (ann instanceof OneToMany) {
    if (((OneToMany) ann).fetch() == FetchType.LAZY) {
    return new HibernateProxiedToNullXmlAccessor<Object, Object>(bean, f);
    }
    }
    }
    return new Accessor.FieldReflection(f);
    }
    
    /** 
    * @inheritdoc
    */
    public Accessor createPropertyAccessor(Class bean, Method getter, Method setter)
    throws JAXBException {
    if (getter == null) {
    return new Accessor.SetterOnlyReflection(setter);
    }
    if (setter == null) {
    return new Accessor.GetterOnlyReflection(getter);
    }
    return new Accessor.GetterSetterReflection(getter, setter);
    }
    }
    
    
  4. Declaring the previously created AccessorFactory on package level (package-info.java) of your Hibernate entities
    @XmlAccessorFactory(XmlAccessorFactoryImpl.class)
    package x.y.z;
    import x.y.XmlAccessorFactoryImpl;
    
    import com.sun.xml.bind.XmlAccessorFactory;
    
    
    

  5. Implementing com.sun.xml.ws.developer.JAXBContextFactory to create your JAXBContext with "the support for XMLAccessor turned on"
    public class JaxbContextFactory implements JAXBContextFactory {
    
    /**
    * @inheritdoc
    */
    public JAXBRIContext createJAXBContext(SEIModel seiModel, List<Class> classes,
    List<TypeReference> typeRefs) throws JAXBException {
    return ContextFactory.createContext(classes.toArray(new Class[classes.size()]), typeRefs,
    null, null, false, new RuntimeInlineAnnotationReader(), true, false);
    }
    
    }
    

  6. Making your JAX-WS use the previously JAXBContext by annotating it with @UsesJAXBContext
    @WebService(name="XYZWS",
    serviceName="XYZWSService")
    @XmlJavaTypeAdapter(XYZServiceEndpoint.XmlAdapter.class)
    @UsesJAXBContext(JaxbContextFactory.class)
    public interface XYZService {
    
That's it!!! From now, you can forget about the famous lazy exception and completely control what will be loaded by Hibernate.

1 comment: