jackson-datatype-hibernate icon indicating copy to clipboard operation
jackson-datatype-hibernate copied to clipboard

Using Hibernate Module for lazy loading does not work with @JsonIdentityInfo

Open aschneid75 opened this issue 11 years ago • 12 comments

I'm not sure if this is because the proxy does not have the annotation on it or if it is something else.

We have several entities with cyclic dependencies and were originally defined as EAGER loading. We added the @JsonIdentityInfo on those classes and they were serializing fine.

I changed several of them to LAZY loading for performance reasons, and now it seems that when the classes that are proxied are serialized, the @JsonIdentityInfo is not being honored by the Hibernate 4 Module.

We are using JBoss 7.2.0 with RestEasy. I added the appropriate dependencies to my Maven pom, wrote a custom provider and simply instantiated an ObjectMapper and registered the Hibernate4Module.

When I retrieve the object via a call to RestEasy, it attempt to serialize it using the ObjectMapper I created, but it ends with an infinite loop and eventually throws an exception. Similar to the way Jackson 1 used to react with cyclic dependencies. It prints the same two error lines multiple times in the stack trace (I am cutting it off after a few....it's probably several hundred times repeated:

23:46:41,588 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[default-host].[/mission_planning_war].[Resteasy]] (http-/127.0.0.1:8080-1) JBWEB000236: Servlet.service() for servlet Resteasy threw exception: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.JsonMappingException$Reference from [Module "deployment.elint_ear-0.0.1-SNAPSHOT.ear.mission_planning_war-0.0.1-SNAPSHOT.war:main" from Service Module Loader]
at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:190) [jboss-modules.jar:1.2.0.CR1]
at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:468) [jboss-modules.jar:1.2.0.CR1]
at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:456) [jboss-modules.jar:1.2.0.CR1]
at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:398) [jboss-modules.jar:1.2.0.CR1]
at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:120) [jboss-modules.jar:1.2.0.CR1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:613) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:142) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.datatype.hibernate4.HibernateProxySerializer.serialize(HibernateProxySerializer.java:75) [jackson-datatype-hibernate4-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.datatype.hibernate4.HibernateProxySerializer.serialize(HibernateProxySerializer.java:24) [jackson-datatype-hibernate4-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:569) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:597) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:142) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:117) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:23) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:186) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:569) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:597) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:142) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.datatype.hibernate4.HibernateProxySerializer.serialize(HibernateProxySerializer.java:75) [jackson-datatype-hibernate4-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.datatype.hibernate4.HibernateProxySerializer.serialize(HibernateProxySerializer.java:24) [jackson-datatype-hibernate4-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:569) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:597) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:142) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:117) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:23) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:186) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:569) [jackson-databind-2.2.1.jar:2.2.1]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:597) [jackson-databind-2.2.1.jar:2.2.1]

aschneid75 avatar Aug 30 '13 06:08 aschneid75

It would be great to get a reproducible test case here: I believe there is a problem, but it is difficult for me to reproduce this without bit of help. With a unit test, it is possible this could be easy to resolve.

cowtowncoder avatar Nov 09 '13 21:11 cowtowncoder

Hi, I had the same issue and here is the entity class that you could use to reproduce:

@Entity
@Table
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class User implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id = 0;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private User linkedUser;

    public int getId(){
        return id;
    }

    public void setId(int id){
        this.id = id;
    }

    public User getLinkedUser(){
        return linkedUser;
    }

    public void setLinkedUser(User linkedUser){
        this.linkedUser = linkedUser;
    }
}

reda-alaoui avatar Jul 30 '14 19:07 reda-alaoui

Hi, I have the same problem as well. A little bit more specific because I am using JAXB annotations (@XmlID an @XmlIDREF).

I will try to check what the problem is and see if I can provide a fix.

/Manuel

ManuelB avatar May 19 '15 06:05 ManuelB

The default BeanSerializer can use a _objectIdSerializer in

    public final void serialize(Object bean, JsonGenerator jgen, SerializerProvider provider)
        throws IOException, JsonGenerationException
    {
       if (_objectIdWriter != null) {
            _serializeWithObjectId(bean, jgen, provider, true);
            return;
        }

This is missing from HibernateProxySerializer:

    @Override
    public void serialize(HibernateProxy value, JsonGenerator jgen, SerializerProvider provider)
        throws IOException, JsonProcessingException
    {
        Object proxiedValue = findProxied(value);
        // TODO: figure out how to suppress nulls, if necessary? (too late for that here)
        if (proxiedValue == null) {
            provider.defaultSerializeNull(jgen);
            return;
        }
        findSerializer(provider, proxiedValue).serialize(proxiedValue, jgen, provider);
    }

I will try to fix this.

/Manuel

ManuelB avatar May 19 '15 06:05 ManuelB

I already found the possibility to prevent loading uninitiated objects and just output the identifier:

        ObjectMapper objectMapper = new ObjectMapper();
        Hibernate4Module hibernate4Module = new Hibernate4Module();
        hibernate4Module.enable(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);
        objectMapper.registerModule(hibernate4Module);

ManuelB avatar May 19 '15 07:05 ManuelB

I just realized that it is quite complex to implement this bug and I currently do not have the time. I tried to change HibernateProxySerializer to extend BeanSerializerBase instead of JsonSerializer<HibernateProxy> but this introduced a whole bunch of other compilation problems. I would still thing this is the way to go.

Here is also a mocking object (Mockito) that I used for a test case:

public class EntityHibernateProxy extends SomeJPAEntity implements HibernateProxy {

    private LazyInitializer lazyInitializer;
    /**
     * 
     */
    private static final long serialVersionUID = 7245130694573745489L;

    public EntityHibernateProxy() {
        lazyInitializer = mock(LazyInitializer.class);
        when(lazyInitializer.isUninitialized()).thenReturn(true);
        when(lazyInitializer.getIdentifier()).thenReturn("d0024148-3040-4fee-a78a-e94fd2449ac6");
        when(lazyInitializer.getEntityName()).thenReturn(StringUtils.uncapitalize(SomeJPAEntity.class.getSimpleName()));
    }

    @Override
    public Object writeReplace() {
        return null;
    }

    @Override
    public LazyInitializer getHibernateLazyInitializer() {
        return lazyInitializer;
    }

}

ManuelB avatar May 19 '15 07:05 ManuelB

Just figured out that the lazy setting won't solve my problem. The following HibernateProxy should reproduce the problem:


@Entity
public class SomeJPAEntity  {
    @Id
    private String id;
    // @ManyToOne not needed because we mock this
    private SomeJPAEntity owner;
    // ... getter setters 

}

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.apache.commons.lang.StringUtils;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class EntityHibernateProxy extends SomeJPAEntity implements HibernateProxy {

    private LazyInitializer lazyInitializer;
    /**
     * 
     */
    private static final long serialVersionUID = 7245130694573745489L;

    public EntityHibernateProxy() {
        this(true);
    }

    public EntityHibernateProxy(final boolean uninitialized) {
        lazyInitializer = mock(LazyInitializer.class);
        when(lazyInitializer.isUninitialized()).thenReturn(uninitialized);
        final String id = "d0024148-3040-4fee-a78a-e94fd2449ac6";
        when(lazyInitializer.getIdentifier()).thenReturn(id);
        when(lazyInitializer.getEntityName()).thenReturn(StringUtils.uncapitalize(SomeJPAEntity.class.getSimpleName()));

        when(lazyInitializer.getImplementation()).thenAnswer(new Answer<Object>() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                SomeJPAEntity entity = new SomeJPAEntity();
                entity.setId(id);
                entity.setOwner(new EntityHibernateProxy(false));
                return entity;
            }});
    }

    @Override
    public Object writeReplace() {
        return null;
    }

    @Override
    public LazyInitializer getHibernateLazyInitializer() {
        return lazyInitializer;
    }
}

// Test case

                EntityHibernateProxy entity = new EntityHibernateProxy(false);
        entity.setId("3cf7a573-f528-440c-83b9-873d7594b373");
        entity.setOwner(entity);

        ObjectMapper objectMapper = new ObjectMapper();
        Hibernate4Module hibernate4Module = new Hibernate4Module();
        hibernate4Module.enable(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);
        objectMapper.registerModule(hibernate4Module);
        if(prettyPrint) {
            objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
        }
        objectMapper.registerModule(new JaxbAnnotationModule());

        StringWriter stringWriter = new StringWriter();
        objectMapper.writeValue(stringWriter, entity);
        String s = stringWriter.toString();

ManuelB avatar May 19 '15 08:05 ManuelB

@ManuelB one quick note: trying to extend BeanSerializerBase would probably not be a good idea, as that really is a complex thing to do, and functionally proxy types are not POJOs but references. But I understand why it would be tempting thing to try.

Thank you for the reproducible test case. Looks like it might be possible to remove dependency to JAXB module as well? That would be helpful just in reducing number of dependencies for building.

cowtowncoder avatar May 19 '15 18:05 cowtowncoder

Hi, I realized that I will need a solution for that. I already have an ugly way of using BeanSerializer and some other quite complex classes.

When it works I will create a pull request it and it can be decided if it makes sense to use it.

/Manuel

ManuelB avatar May 21 '15 11:05 ManuelB

Ok; I can't make test case work as is. It seems to require some additions to xml configs.

cowtowncoder avatar Jan 16 '16 04:01 cowtowncoder

Hi Tatu, I am the original author of some of the test cases. They shouldn't need any xml configs. Unfortunately like I already said I currently don't have the time to look into it.

/Manuel

ManuelB avatar Jan 17 '16 16:01 ManuelB

I've got a similar issue. I resolved it by giving up on this and redesigning everything :)

Sam-Kruglov avatar Dec 28 '17 04:12 Sam-Kruglov