We use Glassfish 2.1 for our application development and have recently switched to use Hibernate as our JPA provider.
Surprisingly, it seemed not easily possible to expose entity beans managed by Hibernate JPA through web services implemented as EJB session beans. I kept getting a LazyInitializationException thrown every time an entity bean with a lazily loaded relationship was returned from one of the web services due to the Hibernate session being terminated before JAXB was trying to marshal the entity beans.
I am aware of the
Open Session in View Pattern that is commonly being portrayed on the Hibernate forums as the appropriate solution to this kind of problem when Hibernate entities are to be passed around in a multi-tier environment. However, I think the pattern is neither elegant nor appropriate for SOA applications built around EJBs (correct if I got that wrong).
Disabling all lazy loading and suppressing related entities with
hibernate.max_fetch_depth is also not an acceptable solution either in pretty much any larger deployment.
The resolution of
JAXB Issue 280 offers a fairly elegant solution to this problem. However, unfortunately, JAXB ignores any
@XmlAccessorFactory annotations unless the property
com.sun.xml.bind.XmlAccessorFactory is set to true on the JAXBContext responsible for marshalling the entities.
The design decision to default this property of the JAXBContext to false and not allow it to be controlled via a system property does not present a problem when manually marshalling JAXB annotated objects, but hinders customization in a managed environments. Fortunately, the fix to
JAX-WS Issue 282 adds the
@UsesJAXBContext annotation. This annotation allows specifying the JAXBContext implementation to be used by a particular web service endpoint.
Together these two fixes provide a good workaround to the Hibernate lazy loading problem in EJB web services. So, here is what I did to fix the problem in our setup.
- Upgrade to Metro 1.5 in order to be able to take advantages of the described improvements in JAXB and JAX-WS. Instructions on how to apply this upgrade to Glassfish can be found here.
- Create an implementation of com.sun.xml.ws.developer.JAXBContextFactory that enables support for @XmlAccessorFactory annotations in the JAXBContext objects it returns.
Code:
package util.ws;
import com.sun.xml.bind.api.JAXBRIContext;
import com.sun.xml.bind.api.TypeReference;
import com.sun.xml.bind.v2.ContextFactory;
import com.sun.xml.ws.api.model.SEIModel;
import com.sun.xml.ws.developer.JAXBContextFactory;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBException;
public class JAXBContextFactoryImpl implements JAXBContextFactory {
public JAXBRIContext createJAXBContext(SEIModel sei, List<Class> classesToBind, List<TypeReference> typeReferences) throws JAXBException {
return ContextFactory.createContext(classesToBind.toArray(new Class[classesToBind.size()]),
new ArrayList<TypeReference>(typeReferences),
null,
sei.getTargetNamespace(),
false,
null,
true,
false,
false);
}
}
- Annotate your web service bean with the @UsesJAXBContext[/item] annotation. Like so,
Code:
--- SNIP ---
@Remote({ExampleFacadeBeanRemote.class})
@WebService(serviceName = "ExampleBeanService")
@UsesJAXBContext(JAXBContextFactoryImpl.class)
@Stateless(name = "ExampleFacade")
@TransactionManagement(TransactionManagementType.CONTAINER)
public class ExampleFacadeBean implements ExampleFacadeBeanRemote {
--- SNIP ---
- Create an implementation of com.sun.xml.bind.v2.runtime.reflect.Accessor that checks if a field of an entity bean is being lazy loaded and if so returns null instead. Then create a matching implementation of com.sun.xml.bind.AccessorFactory that returns your custom Accessor. There is an example provided in the thread of JAXB Issue 280. I have adapted it as follows in order to be able to share the entity class library between the service layer and its client without requiring the client to have Hibernate on the classpath.
Code:
package util.hibernate;
import com.sun.xml.bind.AccessorFactory;
import com.sun.xml.bind.AccessorFactoryImpl;
import com.sun.xml.bind.v2.runtime.reflect.Accessor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HibernateJAXBAccessorFactory implements AccessorFactory {
private final AccessorFactory delegate;
private Method hibernateInitializationCheck;
public HibernateJAXBAccessorFactory() {
this(AccessorFactoryImpl.getInstance());
}
public HibernateJAXBAccessorFactory(AccessorFactory delegate) {
this.delegate = delegate;
try {
Class hibernate = Class.forName("org.hibernate.Hibernate");
hibernateInitializationCheck = hibernate.getMethod("isInitialized", Object.class);
Logger logger = LoggerFactory.getLogger(HibernateJAXBAccessorFactory.class);
logger.info("Detected Hibernate: Enabled hiding of uninitialized lazy objects and collections during XML marshalling.");
} catch(ClassNotFoundException e) {
hibernateInitializationCheck = null;
Logger logger = LoggerFactory.getLogger(HibernateJAXBAccessorFactory.class);
logger.info("Hibernate was not detected: Disabled hiding of uninitialized lazy objects and collections during XML marshalling.");
} catch(Exception e) {
hibernateInitializationCheck = null;
Logger logger = LoggerFactory.getLogger(HibernateJAXBAccessorFactory.class);
logger.warn("Detected Hibernate, but failed to enable hiding of uninitialized lazy objects and collections during XML marshalling.", e);
}
}
@SuppressWarnings("unchecked")
public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly) throws JAXBException {
Accessor accessor = delegate.createFieldAccessor(bean, field, readOnly);
if(hibernateInitializationCheck == null) {
return accessor;
} else {
return new HibernateJAXBAccessor(accessor, hibernateInitializationCheck);
}
}
@SuppressWarnings("unchecked")
public Accessor createPropertyAccessor(Class bean, Method getter, Method setter) throws JAXBException {
Accessor accessor = delegate.createPropertyAccessor(bean, getter, setter);
if(hibernateInitializationCheck == null) {
return accessor;
} else {
return new HibernateJAXBAccessor(accessor, hibernateInitializationCheck);
}
}
}
Code:
package util.hibernate;
import com.sun.xml.bind.api.AccessorException;
import com.sun.xml.bind.v2.runtime.JAXBContextImpl;
import com.sun.xml.bind.v2.runtime.reflect.Accessor;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HibernateJAXBAccessor<B, V> extends Accessor<B, V> {
private Accessor<B, V> delegate;
private final Method hibernateInitializationCheck;
@SuppressWarnings("unchecked")
protected HibernateJAXBAccessor(Accessor<B, V> delegate, Method hibernateInitializationCheck) {
super(delegate.getValueType());
if(delegate == null) {
throw new IllegalArgumentException("delegate must not be null");
} else if(hibernateInitializationCheck == null) {
throw new IllegalArgumentException("hibernateInitializationCheck must not be null");
}
this.delegate = delegate;
this.hibernateInitializationCheck = hibernateInitializationCheck;
}
@Override
public Accessor<B, V> optimize(JAXBContextImpl context) {
delegate = delegate.optimize(context);
return this;
}
@Override
public V get(B bean) throws AccessorException {
return hideLazy(delegate.get(bean));
}
@Override
public void set(B bean, V value) throws AccessorException {
delegate.set(bean, value);
}
protected V hideLazy(V value) {
try {
boolean isInitialized = (Boolean) hibernateInitializationCheck.invoke(null, new Object[]{value});
if(isInitialized) {
return value;
} else {
return null;
}
} catch (Exception e) {
Logger logger = LoggerFactory.getLogger(HibernateJAXBAccessor.class);
logger.error("Failed to determine state of Hibernate object or collection, assuming " + value + " is initialized", e);
return null;
}
}
}
- Now all that remains is creating an appropriate package-info.java in the package containing your XML annotated entity beans and you are set to go.
Code:
@XmlAccessorFactory(HibernateJAXBAccessorFactory.class)
package example.beans.entities;
import util.hibernate.HibernateJAXBAccessorFactory;
import com.sun.xml.bind.XmlAccessorFactory;
- Say goodbye to the LazyInitializationException.
This solution is provided in the hope that it may be useful to others who have encountered the same problem. Constructive comments and improvements to this solution are of course much appreciated :).