JAX-WS +休眠+ JAXB:如何避免编组期间LazyInitializationExcepti

2019-10-19 15:18发布

我有一个JAX-WS应用,它返回的数据对象,从休眠数据库后端(甲骨文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

当编组尝试为SomeDependency类这是一个HibernateProxy实例的“代码”属性获得价值它发生。

我看到现在的解决方案是增加某种“过滤器”的编组,如果对象是HibernateProxy与否的实例期间检查的。 如果是HibernateProxy实例的过滤器处理它,如果没有,只是离开它的默认行为。

我怎样才能做到这一点? 使用XmlJavaTypeAdapter类? 或使用com.sun.xml.internal.bind.v2.runtime.reflect.Accessor?

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

注:我在重复使用其他Web服务和外JAX-WS相同的Hibernate代码和POJO,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:我也尝试过使用访问器类中的第二个解决方案。 这是我访问:

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;

现在我需要启用对JAXB上下文定制AccessorFactory /访问器的支持。 我尝试添加自定义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。

Answer 1:

我想注释@XmlTransient是为这一点。 将它添加到你的财产someDependency使JAXB忽略这个字段。

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

更新以下意见:如果你必须去的适配器的选择,我的猜测是,你必须:

  1. 创建实体用户扩展XmlAdapter一个新的适配器
  2. 在您的适配器要求每个属性和使用方法编组Hibernate.isInitialized(yourObject.getSomeDependency())来测试是否该协会已经呼吁编组或之前没有被加载。
  3. 通过将其声明@XmlJavaTypeAdapter用适当的属性到你的实体用户

也许它可以通过直接创造财产的适配器来完成someDependency但你可能会想到一个LazyInitializationException中时,JAXB将尝试将财产传给适配器。



Answer 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是终局的,不能被覆写...所以你必须复制代码。

我personnaly为了根据对象的上下文和内容来修改编组合并这两个approches。



Answer 3:

只需用Javaassist来以防万一这一个替代访问器$ 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();
  }
}


文章来源: JAX-WS + Hibernate + JAXB: How to avoid LazyInitializationException during marshalling