JSON
serialization format is all the rage
these days when it comes to making Ajax calls from the browser. And Jackson JSON-processor
is
probably the best and most popular serializer written in Java for converting
domain objects into JSON. Hibernate
is, of course, the most popular object-relational
mapping framework. And the problem is that those three don’t play well
together...
The main obstacle is lazily initialized object properties:
whenever a graph of object is retrieved from Hibernate, it is necessary to limit
the amount of data, thus some properties must be lazy-loaded. And this is where
the things begin to break down. There is really no easy workaround: if the
session remains open during JSON serialization, then Jackson is going to walk
through your object graph and lazily instantiate every object, potentially
loading the whole database in memory; if the session is closed, then the moment
a "lazy" property is encountered the
org.hibernate.LazyInitializationException
is going to be
thrown.
I was really surprised to discover that there has been really
no acceptable solution to the problem yet. Some folks advocate building a DTO
layer for doing the conversion in code, some suggest inspecting
JPA/Hibernate
annotations on entities and reject any property
marked as
FetchType.LAZY
, which effectively kills
object graph navigation as FetchType.LAZY
is the dominant form of
connecting entities together. The issue is well documented in Jackson JIRA
Item 276.
So, I decided to dig into Jackson internals and come up with
a solution. The main requirement was that the solution must not interfere with
the domain model and be transparent. It should also satisfy the following
criteria:
1.
Must
transparently detect if a property is “lazy” and serialize it as “null”.
2.
Must not depend
only on annotation mappings. If an entity is mapped in XML, that should be fine
too.
3.
Must support
byte-code instrumented
Hibernate entities, i.e. the ones that allow making
simple fields lazy (not just entity properties or collections).
I’ve accomplished this by creating a custom Jackson
SerializerFactory
: HibernateAwareSerializerFactory
. Here is
its source code. I've put as many comments as I could, so it should be
self-documenting.
import javax.persistence.Transient;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.introspect.BasicBeanDescription;
import org.codehaus.jackson.map.ser.BeanPropertyWriter;
import org.codehaus.jackson.map.ser.BeanSerializerFactory;
import org.codehaus.jackson.type.JavaType;
import org.hibernate.bytecode.javassist.FieldHandled;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.collection.PersistentMap;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationUtils;
/**
* This is the key class in enabling graceful handling of Hibernate managed entities when
* serializing them to JSON.
* <p/>
* The key features are:
* 1) Non-initialized properties will be rendered as {@code null} in JSON to prevent
* "lazy-loaded" exceptions when the Hibernate session is closed.
* 2) {@link Transient} properties not be rendered at all as they often present back door
* references to non-initialized properties.
*
* @author Kyrill Alyoshin
*/
public class HibernateAwareSerializerFactory extends BeanSerializerFactory {
/**
* Name of the property added during build-time byte-code instrumentation
* by Hibernate. It must be filtered out.
*/
private static final String FIELD_HANDLER_PROPERTY_NAME = "fieldHandler";
@Override
@SuppressWarnings("unchecked")
public JsonSerializer<Object> createSerializer(JavaType type, SerializationConfig config) {
Class<?> clazz = type.getRawClass();
//check for all Hibernate proxy invariants and build custom serializers for them
if (PersistentCollection.class.isAssignableFrom(clazz)) {
return new PersistentCollectionSerializer(type, config);
}
if (HibernateProxy.class.isAssignableFrom(clazz)) {
return new HibernateProxySerializer(type, config);
}
//Well, then it is not a Hibernate proxy
return super.createSerializer(type, config);
}
/**
* The purpose of this method is to filter out {@link Transient} properties of the bean
* from JSON rendering.
*/
@Override
protected List<BeanPropertyWriter> filterBeanProperties(SerializationConfig config,
BasicBeanDescription beanDesc,
List<BeanPropertyWriter> props) {
//filter out standard properties (e.g. those marked with @JsonIgnore)
props = super.filterBeanProperties(config, beanDesc, props);
filterInstrumentedBeanProperties(beanDesc, props);
//now filter out the @Transient ones as they may trigger "lazy" exceptions by
//referencing non-initialized properties
List<String> transientOnes = new ArrayList<String>();
//BeanUtils and AnnotationUtils are utility methods that come from
//the Spring Framework
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(beanDesc.getBeanClass())) {
Method getter = pd.getReadMethod();
if (getter != null && AnnotationUtils.findAnnotation(getter, Transient.class) != null) {
transientOnes.add(pd.getName());
}
}
//remove transient
for (Iterator<BeanPropertyWriter> iter = props.iterator(); iter.hasNext();) {
if (transientOnes.contains(iter.next().getName())) {
iter.remove();
}
}
return props;
}
private void filterInstrumentedBeanProperties(BasicBeanDescription beanDesc,
List<BeanPropertyWriter> props) {
//all beans that have build-time instrumented lazy-loaded properties
//will implement FieldHandled interface.
if (!FieldHandled.class.isAssignableFrom(beanDesc.getBeanClass())) {
return;
}
//remove fieldHandler bean property from JSON serialization as it causes
//infinite recursion
for (Iterator<BeanPropertyWriter> iter = props.iterator(); iter.hasNext();) {
if (iter.next().getName().equals(FIELD_HANDLER_PROPERTY_NAME)) {
iter.remove();
}
}
}
/**
* The purpose of this class is to perform graceful handling of custom Hibernate collections.
*/
private class PersistentCollectionSerializer extends JsonSerializer<Object> {
private final JavaType type;
private final SerializationConfig config;
private PersistentCollectionSerializer(JavaType type, SerializationConfig config) {
this.type = type;
this.config = config;
}
@Override
@SuppressWarnings("unchecked")
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
//avoid lazy initialization exceptions
if (!((PersistentCollection) value).wasInitialized()) {
jgen.writeNull();
return;
}
//construct an actual serializer from the built-in ones
BasicBeanDescription beanDesc = config.introspect(type.getRawClass());
Class<?> clazz = type.getRawClass();
JsonSerializer<Object> serializer;
if (PersistentMap.class.isAssignableFrom(clazz)) {
serializer = (JsonSerializer<Object>) buildMapSerializer(type, config, beanDesc);
}
else {
serializer = (JsonSerializer<Object>) buildCollectionSerializer(type, config, beanDesc);
}
//delegate serialization to a built-in serializer
serializer.serialize(value, jgen, provider);
}
}
/**
* The purpose of this class is to perform graceful handling of HibernateProxy objects.
*/
private class HibernateProxySerializer extends JsonSerializer<Object> {
private final JavaType type;
private final SerializationConfig config;
private HibernateProxySerializer(JavaType type, SerializationConfig config) {
this.type = type;
this.config = config;
}
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
if (((HibernateProxy) value).getHibernateLazyInitializer().isUninitialized()) {
jgen.writeNull();
return;
}
BasicBeanDescription beanDesc = config.introspect(type.getRawClass());
JsonSerializer<Object> serializer = findBeanSerializer(type, config, beanDesc);
//delegate serialization to a build-in serializer
serializer.serialize(value, jgen, provider);
}
}
}
And this point this custom SerializerFactory needs to be
registered with the root Jackson ObjectMapper. If you're using IoC container
(like Spring) to wire up your project infrastructure, it is best to create your
own ObjectMapper to tweak it a bit:
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
/**
* This class extends {@code ObjectMapper} class of the Jackson framework to provide
* minor customizations:
* <ul>
* <li>To set a custom {@link HibernateAwareSerializerFactory}</li>
* <li>To relax Jackson handling of unknown class types</li>
* </ul>
* <p/>
* <em>Note:</em> Due to the nature {@code ObjectMapper} class
* those customization could not be done through the Spring Framework.
*
* @author Kyrill Alyoshin
* @see HibernateAwareSerializerFactory
*/
public class HibernateAwareObjectMapper extends ObjectMapper {
public HibernateAwareObjectMapper() {
setSerializerFactory(new HibernateAwareSerializerFactory());
configure(Feature.FAIL_ON_EMPTY_BEANS, false);
}
public void setPrettyPrint(boolean prettyPrint) {
configure(Feature.INDENT_OUTPUT, prettyPrint);
}
}
And this is it.
I do have a comprehensive integration
testing suite in my project to test these classes.
The only issue that still needs to be address is
bi-directional navigation. Jackson will run out of stack when it attempts to
serialize bi-directional entities. So, it is important to mark one side of the
association with @JsonIgnore annotation. After this is done, you should be able
to serialize your “nurtured” domain model into JSON using Jackson without
resorting to useless DTO layers or one-off solutions.
Comments
I tried your approach handling.
First the the class HibernateAwareSerializerFactory won't compile for me. I had either to change the two line containing config.introspect(type.getRawClass()); to config.introspectClassAnnotations(type.getRawClass()); or to config.introspect(type);
Then the class get's compiled without any errors. I tried to use the Mapper in my spring app using:
However I'm still running in:
org.codehaus.jackson.map.JsonMappingException: failed to lazily initialize a collection of role ...
Any ideas?
regards,
Frank
wow that was fast :-). Yes you are right, using the 1.5.7er version it compiles without any failures. However I'm getting the same failure concerning lazy loading. The HibernateAwareObjectMapper constructor get's called during startup, but not the HibernateAwareSerializerFactory during a request. You can find my spring config here: http://pastebin.com/k8n86ABe.
thanks for you help,
Frank
I'm sorry still having the same problem with lazy initialization:
org.codehaus.jackson.map.JsonMappingException: failed to lazily initialize a collection of role: com.loiane.model.KFCase.KFCaseSections, no session or session was closed
I copied my spring config again ... http://pastebin.com/dexHzYkk.
Any more ideas?
regards,
Frank
as a view within your InternalResourceViewResolver.
It is my understanding that MappingJacksonHttpMessageConverter is for RESTful WS calls. If you just want to get a web app going with some JSON spitting back-end, then MappingJacksonJsonView is the way to go.
bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"
property name="objectMapper" ref="hibernateAwareObjectMapper"
bean
regards,
Frank
I added the beans and db config ... http://pastebin.com/MN5mPi0C.
I'm pretty new to spring mvc :-)
regards,
Frank
I have modified filterBeanProperties to exclude those getMethods whose Field is marked Transient with Transient java keyword.
/**
* The purpose of this method is to filter out {@link Transient} properties of the bean
* from JSON rendering.
*/
@Override
protected List filterBeanProperties(SerializationConfig config,
BasicBeanDescription beanDesc,
List props) {
//filter out standard properties (e.g. those marked with @JsonIgnore)
props = super.filterBeanProperties(config, beanDesc, props);
filterInstrumentedBeanProperties(beanDesc, props);
//now filter out the @Transient ones as they may trigger "lazy" exceptions by
//referencing non-initialized properties
List transientOnes = new ArrayList();
//BeanUtils and AnnotationUtils are utility methods that come from
//the Spring Framework
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(beanDesc.getBeanClass())) {
try {
Field fld = beanDesc.getBeanClass().getDeclaredField(pd.getName());
if( Modifier.isTransient( fld.getModifiers() )){
Method getter = pd.getReadMethod();
if ( getter != null ) {
transientOnes.add(pd.getName());
}
continue;
}
} catch (NoSuchFieldException e) {
//Ignore it as some reflected field may not be available
//e.printStackTrace();
}
catch (Exception e) {
//TODO
e.printStackTrace();
}
Method getter = pd.getReadMethod();
if (getter != null && AnnotationUtils.findAnnotation(getter, Transient.class) != null) {
transientOnes.add(pd.getName());
}
}
//remove transient
for (Iterator iter = props.iterator(); iter.hasNext();) {
if (transientOnes.contains(iter.next().getName())) {
iter.remove();
}
}
return props;
}
Regards,
Rashid
Is there a way to make it work only with already initialized entities and not attempt to initialize anything (or am I wrong and it doesn't)? I'd like the service to decide what to initialize and when
Can it be done? (or did I miss something and you already do that) :)
Thanks again