我有一个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属性。我不希望它被退回。
下面是userservicerresponse类的定义:
@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会话已关闭。当它尝试编组响应时,我得到以下异常:
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类的"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,事实上,有很多这样的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
据我所知,这发生在它试图封送一个初始化的hibernate集合时,例如: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));
}
}
package-info.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注释什么也没做…
有谁知道怎么让它工作吗?或者如何设置"com.sun.xml.bind. xml"。XmlAccessorFactory" JAXBContext属性在JAX-WS内为true ?
顺便说一句,我忘了说,我把它部署到JBoss EAP 6.2。
我认为注释@XmlTransient
是为了这个。将它添加到属性someDependency
中,使JAXB忽略此字段。
@XmlTransient
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "SM_DPND_ID)
public SomeDependency getSomeDependency() {...}
更新以下注释:如果您必须选择适配器选项,我猜您必须:
- 为实体User扩展XmlAdapter 创建一个新的适配器
- 在适配器中为每个属性调用编组程序,并使用
Hibernate.isInitialized(yourObject.getSomeDependency())
方法来测试在调用编组程序之前是否已加载关联。 - 通过将具有适当属性的
@XmlJavaTypeAdapter
添加到您的实体来声明它。
也许可以通过直接为属性someDependency
创建适配器来完成,但是当JAXB试图将属性传递给适配器时,您可能会期望出现LazyInitializationException。
有两种解决方案,它们可以组合在一起,以获得对编组的最佳控制。
- 提供一个AccessorFactory来创建一个自定义的Accessor。在此访问器中,重写
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,这样你就可以返回你想要的注释,这取决于Class, Field等…因此,如果不希望对对象进行封送,可以返回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
是final,不能重写…所以你必须复制代码。
我个人将这两种方法结合起来,以便根据对象的上下文和内容修改编组。
使用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();
}
}