JAX-WS + Hibernate + JAXB: How to avoid LazyInitia

2019-08-12 15:58发布

问题:

I have a JAX-WS application, which returns data objects, that were fetched from a Hibernate Database backend (Oracle 10g or Oracle 11g). I use javax.persistence.criteria.CriteriaQuery for that. It works fine, unless the object has dependencies, which should not be returned for some specific queries, e.g.:

@Immutable
@Entity
@Table(schema = "some_schema", name = "USER_VW")
public class User implements Serializable {

  ...

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "PRFL_ID")
  public Profile getProfile() {...}

  public void setProfile(Profile profile) {...}

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "SM_OTH_TP_ID")
  public SomeOtherType getSomeOtherType() {...}

  public void setSomeOtherType(SomeOtherType otherType) {...}

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "SM_DPND_ID)
  public SomeDependency getSomeDependency() {...}

  public void setSomeDependency(SomeDependency dependency) {...}

...
}

Here is my criteria query:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> criteria = cb.createQuery(User.class);
criteria.distinct(true);
Root<User> user = criteria.from(User.class);
Join<User, Profile> profileJoin = user.join("profile", JoinType.INNER);
user.fetch("someOtherType", JoinType.LEFT);
criteria.select(user);
Predicate inPredicate = profileJoin.get("profileType").in(types);
criteria.where(inPredicate);

NOTE: I don't fetch SomeDependency property. I don't want it to be returned.

And here is the definition of the UserServiceResponse class:

@XmlRootElement(name = "UserServiceResponse", namespace = "...")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "UserServiceResponse", namespace = "...")
public class UserServiceResponse {

@XmlElementWrapper(name = "users")
@XmlElement(name = "user")
private final Collection<User> users;

...

Then JAXB finds that the Hibernate Session has been closed. When it tries to marshall the response I get the following exception:

Caused by: org.hibernate.LazyInitializationException: could not initialize proxy - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:164) [hibernate-core-4.2.7.SP1-redhat-3.jar:4.2.7.SP1-redhat-3]
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:285) [hibernate-core-4.2.7.SP1-redhat-3.jar:4.2.7.SP1-redhat-3]
    at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:185) [hibernate-core-4.2.7.SP1-redhat-3.jar:4.2.7.SP1-redhat-3]

    at com.myproject.model.user.entity.SomeDependency_$$_jvsteec_98.getCode(SomeDependency_$$_jvsteec_98.java)
...
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:494)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:323)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:251)
    at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:74) [jboss-jaxb-api_2.2_spec-1.0.4.Final-redhat-2.jar:1.0.4.Final-redhat-2]
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.writeObject(JAXBEncoderDecoder.java:612) [cxf-rt-databinding-jaxb-2.7.7.redhat-1.jar:2.7.7.redhat-1]
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:240) [cxf-rt-databinding-jaxb-2.7.7.redhat-1.jar:2.7.7.redhat-1]
    ... 32 more

It happens when marshaller tries to get value for the "code" property of SomeDependency class which is a HibernateProxy instance.

The solution that I see for now is to add some kind of "filter" which checks during marshalling if the object is instance of HibernateProxy or not. If it is a HibernateProxy instance the filter handle it, if not, just leaves its default behavior.

How can I do that? Using a XmlJavaTypeAdapter class? Or using com.sun.xml.internal.bind.v2.runtime.reflect.Accessor?

If anyone can tell me any other way to solve my problem I would be appreciated.

NOTE: I'm reusing the same Hibernate code and POJOs, inside JAX-WS in other web-services and outside JAX-WS in other modules of the application, where lazy loading is an advantage.

UPDATE:

I've tried using XmlJavaTypeAdapter but it didn't work for me. I created a new adapter - HibernateProxyAdapter which extends XmlJavaTypeAdapter. User entity is not the only POJO that I have, in fact, there are a lot of them. To ensure that the adapter is applied to all of them I added it on the package level.

@XmlJavaTypeAdapters(
    @XmlJavaTypeAdapter(value=HibernateProxyAdapter.class, type=HibernateProxy.class)
)
package com.myproject.model.entity;

Here is the adapter:

public class HibernateProxyAdapter extends XmlJavaTypeAdapter<Object, Object> {

    public Object unmarshal(Object v) throws Exception {
        return null; // there is no need to unmarshall HibernateProxy instances
    }

    public Object marshal(Object v) throws Exception {
        if (v != null) {
            if ( v instanceof HibernateProxy ) {
                LazyInitializer lazyInitializer = ((HibernateProxy) v ).getHibernateLazyInitializer();
                if (lazyInitializer.isUninitialized()) {
                    return null;
                } else {
                    // do nothing for now
                }
            } else if ( v instanceof PersistentCollection ) {
                if(((PersistentCollection) v).wasInitialized()) {
                    // got an initialized collection
                } else {
                    return null;
                }
            }
        }
        return v;
    }
}

Now i'm getting another exception:

Caused by: javax.xml.bind.JAXBException: class org.hibernate.collection.internal.PersistentSet nor any of its super class is known to this context.
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getBeanInfo(JAXBContextImpl.java:588)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:648)
    ... 57 more 

As I understand, this happens when it tries to marshal an initialized hibernate collection, e.g.: org.hibernate.collection.internal.PersistentSet. I don't get the reason... PersistentSet implements Set interface. I thought JAXB should know how to handle it. Any ideas?

UPDATE 2: I have also tried the second solution using Accessor class. Here is my accessor:

public class JAXBHibernateAccessor extends Accessor {

    private final Accessor accessor;

    protected JAXBHibernateAccessor(Accessor accessor) {
        super(accessor.getValueType());
        this.accessor = accessor;
    }

    @Override
    public Object get(Object bean) throws AccessorException {
        return Hibernate.isInitialized(bean) ? accessor.get(bean) : null;
    }

    @Override
    public void set(Object bean, Object value) throws AccessorException {
        accessor.set(bean, value);
    }
}

AccessorFactory...

public class JAXBHibernateAccessorFactory implements AccessorFactory {

    private final AccessorFactory accessorFactory = AccessorFactoryImpl.getInstance();

    @Override
    public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly) throws JAXBException {
        return new JAXBHibernateAccessor(accessorFactory.createFieldAccessor(bean, field, readOnly));
    }

   @Override
   public Accessor createPropertyAccessor(Class bean, Method getter, Method setter) throws JAXBException {
        return new JAXBHibernateAccessor(accessorFactory.createPropertyAccessor(bean, getter, setter));
   }
}

package-info.java ...

@XmlAccessorFactory(JAXBHibernateAccessorFactory.class)
package com.myproject.model.entity;

Now I need to enable custom AccessorFactory/Accessor support on the JAXB context. I tried adding custom JAXBContextFactory to the web service definition but it didn't work...

@WebService
@UsesJAXBContext(JAXBHibernateContextFactory.class)
public interface UserService {
...
}

and here is my contextFactory

public class JAXBHibernateContextFactory implements JAXBContextFactory {

    @Override
    public JAXBRIContext createJAXBContext(@NotNull SEIModel seiModel, @NotNull List<Class> classes,
                                       @NotNull List<TypeReference> typeReferences) throws JAXBException {
        return ContextFactory.createContext(classes.toArray(new Class[classes.size()]), typeReferences,
            null, null, false, new RuntimeInlineAnnotationReader(), true, false, false);
    }
}

I don't know why but createJAXBContext method is never invoked. Looks like @UsesJAXBContext annotation does nothing...

Does anyone know how to make it work? Or how can I set the "com.sun.xml.bind.XmlAccessorFactory" JAXBContext property to true inside JAX-WS?

BTW, I forgot to mention, I deploy it to JBoss EAP 6.2.

回答1:

I think annotation @XmlTransient is meant for this. Add it to your property someDependency to make JAXB ignore this field.

@XmlTransient
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "SM_DPND_ID)
public SomeDependency getSomeDependency() {...}

Update following comments: If you must go for the adapter option, my guess is you have to :

  1. Create a new adapter for entity User extending XmlAdapter
  2. In your adapter call the marshaller for each of your property and use method Hibernate.isInitialized(yourObject.getSomeDependency()) to test if the association has been loaded before calling the marshaller or not.
  3. declare it by adding @XmlJavaTypeAdapter with the proper attribute to your entity User

Maybe it can be done by creating directly an adapter for the property someDependency but you might expect a LazyInitializationException when JAXB will try to pass the property to the adapter.



回答2:

There are 2 solutions to that, which can be combined in order to have finest control of marshalling.

  • Provide an AccessorFactory which will create a custom Accessor. In this Accessor, you override

public abstract ValueT get(BeanT bean) throws AccessorException;

and if the pojo is not initialized, return null :

if (!Hibernate.isInitialized(valueT)) { return null; }

Note there is an annoying optimize method :

public Accessor<BeanT,ValueT> optimize(@Nullable JAXBContextImpl context) { return this; }

which can replace your custom Accessor, depending on the Accessor you override (see FieldReflection).

That is one of the solution described in question, but I think you forgot the following initialization of JAXBContext :

HashMap<String, Object> props = new HashMap<String, Object>(); props.put(JAXBRIContext.XMLACCESSORFACTORY_SUPPORT, true); JAXBContext jaxbContext = JAXBContext.newInstance(new Class[] { clazz }, props);

  • The 2nd solution is to override the AnnotationReader of JAXB, that way you can return the annotations you want, depending on the Class, Field, etc... So you can return XmlTransient annotation if you do not want your object to be marshalled. That is done that way :

HashMap<String, Object> props = new HashMap<String, Object>(); props.put(JAXBRIContext.ANNOTATION_READER, new CustomAnnotationReader()); JAXBContext jaxbContext = JAXBContext.newInstance(new Class[] { clazz }, props);

RuntimeInlineAnnotationReader is final and cannot be overriden... so you will have to copy the code.

I personnaly combined those two approches in order to modify marshalling depending on the context and content of the objects.



回答3:

Simply replaced the implementation of Accessor$FieldReflection using Javaassist to come arround this one:

static {
  ClassPool pool = ClassPool.getDefault();
  try {
     CtClass cc = pool.get("com.sun.xml.bind.v2.runtime.reflect.Accessor$FieldReflection");
     CtMethod method = cc.getMethod("get", "(Ljava/lang/Object;)Ljava/lang/Object;");
     method.insertBefore("if (bean instanceof org.hibernate.proxy.HibernateProxy) {\n"
              + "bean = ((org.hibernate.proxy.HibernateProxy)bean).getHibernateLazyInitializer().getImplementation();\n"
              + "}");
    cc.toClass();
  }
  catch (Throwable t) {
      t.printStackTrace();
  }
}