简体   繁体   English

JAX-WS + Hibernate + JAXB:在编组期间如何避免LazyInitializationException

[英]JAX-WS + Hibernate + JAXB: How to avoid LazyInitializationException during marshalling

I have a JAX-WS application, which returns data objects, that were fetched from a Hibernate Database backend (Oracle 10g or Oracle 11g). 我有一个JAX-WS应用程序,该应用程序返回从Hibernate数据库后端(Oracle 10g或Oracle 11g)获取的数据对象。 I use javax.persistence.criteria.CriteriaQuery for that. 我为此使用javax.persistence.criteria.CriteriaQuery。 It works fine, unless the object has dependencies, which should not be returned for some specific queries, eg: 除非对象具有依赖关系,否则它运行良好,除非该依赖关系对于某些特定查询不应返回,例如:

@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. 注意:我不获取SomeDependency属性。 I don't want it to be returned. 我不希望它退回。

And here is the definition of the UserServiceResponse class: 这是UserServiceResponse类的定义:

@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. 然后,JAXB发现休眠会话已关闭。 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. 当marshaller试图获取SomeDependency类的“ code”属性的值时,就会发生这种情况,该类是HibernateProxy实例。

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. 我现在看到的解决方案是添加某种“过滤器”,以在编组期间检查对象是否为HibernateProxy实例。 If it is a HibernateProxy instance the filter handle it, if not, just leaves its default behavior. 如果它是一个HibernateProxy实例,则过滤器将处理它,否则,将保留其默认行为。

How can I do that? 我怎样才能做到这一点? Using a XmlJavaTypeAdapter class? 使用XmlJavaTypeAdapter类? Or using com.sun.xml.internal.bind.v2.runtime.reflect.Accessor? 还是使用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. 注意:我正在其他Web服务的JAX-WS内部和应用程序其他模块的JAX-WS内部重用相同的Hibernate代码和POJO,在这种情况下,延迟加载是一个优势。

UPDATE: 更新:

I've tried using XmlJavaTypeAdapter but it didn't work for me. 我尝试使用XmlJavaTypeAdapter,但对我而言不起作用。 I created a new adapter - HibernateProxyAdapter which extends XmlJavaTypeAdapter. 我创建了一个新的适配器-HibernateProxyAdapter,它扩展了XmlJavaTypeAdapter。 User entity is not the only POJO that I have, in fact, there are a lot of them. 用户实体不是我拥有的唯一POJO,实际上,它们很多。 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, eg: org.hibernate.collection.internal.PersistentSet. 据我了解,这发生在尝试封送已初始化的休眠集合时,例如:org.hibernate.collection.internal.PersistentSet。 I don't get the reason... PersistentSet implements Set interface. 我不知道原因... PersistentSet实现Set接口。 I thought JAXB should know how to handle it. 我以为JAXB应该知道如何处理它。 Any ideas? 有任何想法吗?

UPDATE 2: I have also tried the second solution using Accessor class. 更新2:我也尝试使用Accessor类的第二个解决方案。 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... 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 ... package-info.java ...

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

Now I need to enable custom AccessorFactory/Accessor support on the JAXB context. 现在,我需要在JAXB上下文中启用自定义AccessorFactory / Accessor支持。 I tried adding custom JAXBContextFactory to the web service definition but it didn't work... 我尝试将自定义JAXBContextFactory添加到Web服务定义中,但是没有用...

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

and here is my contextFactory 这是我的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. 我不知道为什么,但是从未调用过createJAXBContext方法。 Looks like @UsesJAXBContext annotation does nothing... 看起来@UsesJAXBContext注释不起作用...

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? 或者如何在JAX-WS中将“ com.sun.xml.bind.XmlAccessorFactory” JAXBContext属性设置为true?

BTW, I forgot to mention, I deploy it to JBoss EAP 6.2. 顺便说一句,我忘了提,我将其部署到JBoss EAP 6.2。

I think annotation @XmlTransient is meant for this. 我认为注释@XmlTransient就是为此目的而设计的。 Add it to your property someDependency to make JAXB ignore this field. 将其添加到属性someDependency以使JAXB忽略此字段。

@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 为实体用户扩展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. 在适配器中,为每个属性调用编组器,并使用方法Hibernate.isInitialized(yourObject.getSomeDependency())来测试是否在调用编组器之前加载了关联。
  3. declare it by adding @XmlJavaTypeAdapter with the proper attribute to your entity User 通过将具有适当属性的@XmlJavaTypeAdapter添加到您的实体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. 也许可以通过直接为属性someDependency创建适配器来完成,但是当JAXB尝试将属性传递给适配器时,您可能会期望出现LazyInitializationException。

There are 2 solutions to that, which can be combined in order to have finest control of marshalling. 有2种解决方案,可以将它们组合在一起,以对编组进行最好的控制。

  • Provide an AccessorFactory which will create a custom Accessor. 提供一个AccessorFactory,它将创建一个自定义的Accessor。 In this Accessor, you override 在此访问器中,您覆盖

public abstract ValueT get(BeanT bean) throws AccessorException;

and if the pojo is not initialized, return null : 如果未初始化pojo,则返回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). 可以替换您的自定义Accessor,具体取决于您覆盖的Accessor(请参见FieldReflection)。

That is one of the solution described in question, but I think you forgot the following initialization of JAXBContext : 那是有问题的解决方案之一,但是我认为您忘记了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. 第二种解决方案是重写JAXB的AnnotationReader,这样您可以根据类,字段等返回所需的注释。因此,如果您不希望将对象编组,则可以返回XmlTransient注释。 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. RuntimeInlineAnnotationReader是最终的,无法覆盖...因此您必须复制代码。

I personnaly combined those two approches in order to modify marshalling depending on the context and content of the objects. 我个人将这两个方法组合在一起,以便根据对象的上下文和内容来修改编组。

Simply replaced the implementation of Accessor$FieldReflection using Javaassist to come arround this one: 只需使用Javaassist替换Accessor $ FieldReflection的实现来解决这个问题:

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();
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM