JAX-WS + Hibernate + JAXB: как избежать исключения LazyInitializationException во время сортировки

1

У меня есть приложение 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 закрыт. Когда он пытается отменить ответ, я получаю следующее исключение:

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

Это происходит, когда маршаллер пытается получить значение для свойства "code" класса SomeDependency, который является экземпляром HibernateProxy.

Решение, которое я вижу сейчас, это добавить какой-то "фильтр", который проверяет во время сортировки, если объект является экземпляром HibernateProxy или нет. Если это экземпляр HibernateProxy, фильтр обрабатывает его, если нет, просто оставляет свое поведение по умолчанию.

Как я могу это сделать? Использование класса XmlJavaTypeAdapter? Или с помощью com.sun.xml.internal.bind.v2.runtime.reflect.Accessor?

Если кто-нибудь скажет мне какой-либо другой способ решить мою проблему, я буду признателен.

ПРИМЕЧАНИЕ. Я повторно использую один и тот же код Hibernate и POJO, внутри 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 interface. Я думал, что 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;

Теперь мне нужно включить пользовательскую поддержку AccessorFactory/Accessor в контексте JAXB. Я попытался добавить пользовательский JAXBContextFactory к определению веб-службы, но он не работал...

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

и вот мой контекст.

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 аннотации ничего не делает...

Кто-нибудь знает, как заставить его работать? Или как я могу установить для свойства JAXBContext "com.sun.xml.bind.XmlAccessorFactory" значение true внутри JAX-WS?

Кстати, я забыл упомянуть, я разворачиваю его в JBoss EAP 6.2.

Теги:
hibernate
web-services
jaxb
jboss

3 ответа

2

Я думаю, для этого предназначена аннотация @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 попытается передать свойство адаптеру.

  • 0
    Ваше решение работает, если вы хотите сделать его временным для всех запросов ... Мне нужно сделать его "временным", только если это экземпляр HibernateProxy. Это происходит, когда вы не получаете его. Итак, это зависит от запроса ...
  • 0
    То есть это означает, что вы повторно используете один и тот же код внутри JAX-WS?
Показать ещё 3 комментария
1

Просто заменил реализацию Accessor $ FieldReflection, используя Javaassist, чтобы прийти вокруг этого:

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

Для этого есть два решения, которые можно комбинировать, чтобы иметь лучший контроль над сортировкой.

  • Предоставьте AccessorFactory, который создаст пользовательский Accessor. В этом 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; }

который может заменить ваш пользовательский Accessor, в зависимости от того, какой Accessor вы переопределите (см. FieldReflection).

Это одно из решений, о которых идет речь, но я думаю, что вы забыли следующую инициализацию JAXBContext:

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

  • Второе решение - переопределить AnnotationReader JAXB, таким образом вы можете вернуть нужные аннотации, в зависимости от класса, поля и т.д. Итак, вы можете вернуть аннотацию 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 является окончательным и не может быть переопределен... поэтому вам придется скопировать код.

Я лично объединил эти два подхода, чтобы изменить сортировку в зависимости от контекста и содержимого объектов.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню