【问题标题】:JAX-WS + Hibernate + JAXB: How to avoid LazyInitializationException during marshallingJAX-WS + Hibernate + JAXB:如何在编组期间避免 LazyInitializationException
【发布时间】:2014-03-19 15:02:52
【问题描述】:

我有一个 JAX-WS 应用程序,它返回从 Hibernate 数据库后端(Oracle 10g 或 Oracle 11g)获取的数据对象。为此,我使用 javax.persistence.criteria.CriteriaQuery。它工作正常,除非对象具有依赖关系,不应该为某些特定查询返回,例如:

@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) {...}

...
}

这是我的条件查询:

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

注意:我不获取 SomeDependency 属性。我不希望它被退回。

这里是 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;

...

然后 JAXB 发现 Hibernate Session 已经关闭。当它尝试编组响应时,我得到以下异常:

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

当 marshaller 尝试获取 SomeDependency 类的“code”属性的值时会发生这种情况,该类是 HibernateProxy 实例。

我现在看到的解决方案是添加某种“过滤器”,它在编组期间检查对象是否是 HibernateProxy 的实例。如果它是 HibernateProxy 实例,则过滤器处理它,如果不是,则保留其默认行为。

我该怎么做?使用 XmlJavaTypeAdapter 类?还是使用 com.sun.xml.internal.bind.v2.runtime.reflect.Accessor?

如果有人能告诉我任何其他方法来解决我的问题,我将不胜感激。

注意:我正在重用相同的 Hibernate 代码和 POJO,在其他 Web 服务的 JAX-WS 内部和应用程序的其他模块的 JAX-WS 外部,延迟加载是一个优势。

更新:

我尝试过使用 XmlJavaTypeAdapter,但它对我不起作用。我创建了一个新的适配器——HibernateProxyAdapter,它扩展了 XmlJavaTypeAdapter。用户实体并不是我唯一拥有的 POJO,其实还有很多。为了确保适配器适用于所有这些,我在包级别添加了它。

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

这是适配器:

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

现在我又遇到了一个异常:

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 

据我了解,当它尝试编组初始化的休眠集合时会发生这种情况,例如:org.hibernate.collection.internal.PersistentSet。我不明白原因...... PersistentSet 实现了 Set 接口。我认为 JAXB 应该知道如何处理它。有什么想法吗?

更新 2: 我还尝试了使用 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));
   }
}

包信息.java ...

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

现在我需要在 JAXB 上下文中启用自定义 AccessorFactory/Accessor 支持。我尝试将自定义 JAXBContextFactory 添加到 Web 服务定义中,但它不起作用...

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

这是我的 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);
    }
}

我不知道为什么,但从未调用过 createJAXBContext 方法。看起来 @UsesJAXBContext 注释什么都不做......

有人知道如何让它工作吗? 或者如何在 JAX-WS 中将“com.sun.xml.bind.XmlAccessorFactory”JAXBContext 属性设置为 true?

顺便说一句,我忘了提,我将它部署到 JBoss EAP 6.2。

【问题讨论】:

    标签: java web-services hibernate jboss jaxb


    【解决方案1】:

    我认为注释@XmlTransient 就是为此而生的。将其添加到您的属性 someDependency 以使 JAXB 忽略此字段。

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

    更新以下 cmets: 如果您必须选择适配器选项,我猜您必须:

    1. 为实体 User 扩展 XmlAdapter 创建一个新适配器
    2. 在您的适配器中为您的每个属性调用编组器,并使用方法Hibernate.isInitialized(yourObject.getSomeDependency()) 在调用编组器之前测试关联是否已加载。
    3. 通过将具有适当属性的@XmlJavaTypeAdapter 添加到您的实体用户来声明它

    也许可以通过直接为属性 someDependency 创建一个适配器来完成,但是当 JAXB 尝试将属性传递给适配器时,您可能会期望发生 LazyInitializationException。

    【讨论】:

    • 如果你想让它对所有查询都是瞬态的,你的解决方案就可以工作......我需要让它“瞬态”,只有当它是一个 HibernateProxy 实例时。当您不获取它时会发生这种情况。所以,这取决于查询...
    • 所以这意味着您在 JAX-WS 中重复使用相同的代码?
    • 如果您指的是 Hibernate 代码和 POJO,那么我的答案是肯定的,我在我的应用程序的其他模块中的 JAX-WS 之外和其他 Web 服务的 JAX-WS 内部重用它们。如果我需要在某个时候获取 SomeDependency,您的解决方案将不起作用。正如我已经说过的,它应该取决于查询
    • 我明白了。从您的原始帖子中,我认为您只有一个使用此代码的 Web 服务...用于其他非 WS 的地方。
    • 我已经添加了 XmlJavaTypeAdapter 解决方案。请参阅上面的详细信息。
    【解决方案2】:

    对此有 2 种解决方案,可以将它们组合起来以实现对编组的最佳控制。

    • 提供一个将创建自定义访问器的 AccessorFactory。在此访问器中,您覆盖

    public abstract ValueT get(BeanT bean) throws AccessorException;

    如果 pojo 未初始化,则返回 null :

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

    注意有一个烦人的优化方法:

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

    它可以替换您的自定义访问器,具体取决于您覆盖的访问器(请参阅 FieldReflection)。

    这是所描述的解决方案之一,但我认为您忘记了 JAXBContext 的以下初始化:

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

    • 第二个解决方案是覆盖 JAXB 的 AnnotationReader,这样您就可以根据类、字段等返回所需的注释......所以如果您不希望您的对象是,您可以返回 XmlTransient 注释编组的。 就是这样完成的:

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

    RuntimeInlineAnnotationReader 是最终的,不能被覆盖...所以你必须复制代码。

    我个人将这两种方法结合起来,以便根据对象的上下文和内容修改编组。

    【讨论】:

      【解决方案3】:

      只需使用 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();
        }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-07-23
        • 1970-01-01
        • 2011-05-23
        • 1970-01-01
        • 2021-09-19
        相关资源
        最近更新 更多