Статья будет полезна всем, кому интересно узнать способ устранения ошибки LazyInitializationException при JAXB сериализации объектов, созданных при помощи Hibernate.
В конце статьи имеется ссылка на исходный код проекта, реализующего предложенное решение — использование custom AccessorFactory.
Для сравнения рассмотрено, как аналогичная проблема решена в популярном JSON-сериализаторе — Jackson.

Допустим, требуется разработать два REST-сервиса: первый возвращает данные о компании и ее поставщиках, второй — о компании и ее клиентах:
(Примечания: компанию, о которой нужно предоставить данные, будем в дальнейшем для простоты определять в базе по ID=0, content-type — по расширению: /HLS/rest/company/suppliers.xml — получить данные о поставщиках в XML.
HLS — context path тестового приложения: hibernate lazy serialization. Ничего умнее не придумал.
Заказчик пожелал получать данные в XML и JSON. По причинам X, Y, Z проектная команда решила для доступа к данным использовать ORM в виде Hibernate, JAXB — для генерации XML, Jackson — для генерации JSON.
Все, начинаем кодировать:
Код для Customer.java Supplier.java приводить не буду, там нет ничего особенного.
В package-info.java определим два fetch profile:
Нетрудно заметить, что «companyWithSuppliers» вытянет из базы поставщиков, а покупателей оставит неинициализированными. Второй profile сделает наоборот.
В DAO будем выставлять нужный fetch profile в зависимости от того, какой сервис вызван:
Разберемся для начала с JSON. Попытка сериализовать объект, возвращенный методом CompanyDAO.getCompany(), стандартным ObjectMapper Jackson'a потерпит неудачу:

Печально, но вполне ожидаемо. Сессия закрылась, Hibernate proxy, которым обернута коллекция suppliers, не может вытянуть данные из базы. Вот было бы здорово, если б такие неинициализированные поля Jackson обрабатывал бы особым образом…
И такое решение есть: jackson-module-hibernate — “add-on module for Jackson JSON processor which handles Hibernate <...> datatypes; and specifically aspects of lazy-loading”. То что надо! Подправим ObjectMapper:
И сериализуем результат работы CompanyDAO.getCompany() нашим новым mapper:

Отлично, все заработало — в итоговом JSON только покупатели и нет поставщиков — неинициализированная коллекция просто занулена. Из недостатков стоит отметить отсутствие поддержки для Hibernate4, но судя по информации на GitHub, эта фича в процессе разработки. Переходим к JAXB.
Разработчики JAXB мыслили слишком глобально, чтобы переживать, что их детище не дружит с каким-то там Hibernate lazy-loading, и никакого штатного средства решения проблемы не предоставили:

Что делать? Проект почти провален.

И сказал Гугл:
Мы подходим к мысли, что в идеале сериализатор должен сам отсекать неинициализированные коллекции — так, так как это делает Jackson.
forum.hibernate и blogs.oracle.
Отпугивало от этих статей отсутствие решения, пригодного для Ctrl+C/Ctrl+V и излишняя перегруженость всякими ненужностями. Так что пришлось содержимое статей творчески доработать и переработать. Результат представлен ниже.
Итак, из упомянутых источников ясно, что нам нужно сделать:
Поехали по пунктам:
Чтобы JAXB начал использовать custom-реализации следует JAXBContext установить специальное свойство «com.sun.xml.bind.XmlAccessorFactory» = true. (оно же JAXBRIContext.XMLACCESSORFACTORY_SUPPORT), которое включает поддержку аннотации @XmlAccessorFactory. В случае использования Spring, сделать это можно не на прямую, а при конфигурировании бина «org.springframework.oxm.jaxb.Jaxb2Marshaller» в свойстве «jaxbContextProperties».
И, наконец, указываем класс нашей реализации при помощи package-level аннотации @XmlAccessorFactory:
После выполнения указанных операций обратимся к нашему сервису для получения данных о компании и покупателях:

Все ок — только покупатели и нет поставщиков. Неинициализированная коллекция с поставщиками занулена нашей AccessorFactory, поэтому JAXB не пытается ее сериализовать и LazyInitializationException не возникает. Дальше можно наводить красоту — убрать суррогатные ключи из выдачи и др. Но это уже другая статья.
В конце, как и обещал, ссылка на исхoдный код рабочего примера (на Spring Web MVC) по теме статьи. В нем используется embedded H2, которая конфигурируется сама при запуске проекта, поэтому отдельной СУБД ставить не нужно. Для тех, кто использует Eclipse + STS plugin, в архиве есть отдельная версия, настроенная под Eclipse и STS.
На этом все, надеюсь, статья кому-нибудь окажется полезной.
В конце статьи имеется ссылка на исходный код проекта, реализующего предложенное решение — использование custom AccessorFactory.
Для сравнения рассмотрено, как аналогичная проблема решена в популярном JSON-сериализаторе — Jackson.
1. А в чем, собственно, проблема?
На нашем абстрактном проекте в базе под управлением некой реляционной СУБД в трех таблицах хранятся данные о компаниях, их поставщиках и покупателях:
Допустим, требуется разработать два REST-сервиса: первый возвращает данные о компании и ее поставщиках, второй — о компании и ее клиентах:
- GET /HLS/rest/company/suppliers HTTP/1.1
Accept: some_content_type - GET /HLS/rest/company/customers HTTP/1.1
Accept: some_content_type
(Примечания: компанию, о которой нужно предоставить данные, будем в дальнейшем для простоты определять в базе по ID=0, content-type — по расширению: /HLS/rest/company/suppliers.xml — получить данные о поставщиках в XML.
HLS — context path тестового приложения: hibernate lazy serialization. Ничего умнее не придумал.
Заказчик пожелал получать данные в XML и JSON. По причинам X, Y, Z проектная команда решила для доступа к данным использовать ORM в виде Hibernate, JAXB — для генерации XML, Jackson — для генерации JSON.
Все, начинаем кодировать:
package ru.habr.zrd.hls.domain; ... @Entity @Table(name = "COMPANY") @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Company { @Id @GeneratedValue private Integer id; @Column(name = "S_NAME") private String name; @OneToMany @JoinColumn(name = "ID_COMPANY") @XmlElementWrapper // Обернем коллекцию дополнительным тегом @XmlElement(name = "supplier") private Set<Supplier> suppliers; @OneToMany @JoinColumn(name = "ID_COMPANY") @XmlElementWrapper // Обернем коллекцию дополнительным тегом @XmlElement(name = "customer") private Set<Customer> customers; // Getters/setters
Код для Customer.java Supplier.java приводить не буду, там нет ничего особенного.
В package-info.java определим два fetch profile:
@FetchProfiles({ @FetchProfile(name = "companyWithSuppliers", fetchOverrides = { @FetchProfile.FetchOverride(entity = Company.class, association = "suppliers", mode = FetchMode.JOIN), }), @FetchProfile(name = "companyWithCustomers", fetchOverrides = { @FetchProfile.FetchOverride(entity = Company.class, association = "customers", mode = FetchMode.JOIN) }) }) package ru.habr.zrd.hls.domain;
Нетрудно заметить, что «companyWithSuppliers» вытянет из базы поставщиков, а покупателей оставит неинициализированными. Второй profile сделает наоборот.
В DAO будем выставлять нужный fetch profile в зависимости от того, какой сервис вызван:
... public class CompanyDAO { public Company getCompany(String fetchProfile) { ... Session session = sessionFactory.getCurrentSession(); session.enableFetchProfile(fetchProfile); Company company = (Company) session.get(Company.class, 0); ... return company; } ...
Разберемся для начала с JSON. Попытка сериализовать объект, возвращенный методом CompanyDAO.getCompany(), стандартным ObjectMapper Jackson'a потерпит неудачу:

Печально, но вполне ожидаемо. Сессия закрылась, Hibernate proxy, которым обернута коллекция suppliers, не может вытянуть данные из базы. Вот было бы здорово, если б такие неинициализированные поля Jackson обрабатывал бы особым образом…
И такое решение есть: jackson-module-hibernate — “add-on module for Jackson JSON processor which handles Hibernate <...> datatypes; and specifically aspects of lazy-loading”. То что надо! Подправим ObjectMapper:
import org.codehaus.jackson.map.ObjectMapper; import com.fasterxml.jackson.module.hibernate.HibernateModule; public class JSONHibernateObjectMapper extends ObjectMapper { public JSONHibernateObjectMapper() { registerModule(new HibernateModule()); //Справедливости ради, стоит отметить, что тут разработчики рекоммендуют //установить еще какие-то малопонятные property, см. ссылку в тексте выше. } }
И сериализуем результат работы CompanyDAO.getCompany() нашим новым mapper:

Отлично, все заработало — в итоговом JSON только покупатели и нет поставщиков — неинициализированная коллекция просто занулена. Из недостатков стоит отметить отсутствие поддержки для Hibernate4, но судя по информации на GitHub, эта фича в процессе разработки. Переходим к JAXB.
Разработчики JAXB мыслили слишком глобально, чтобы переживать, что их детище не дружит с каким-то там Hibernate lazy-loading, и никакого штатного средства решения проблемы не предоставили:

Что делать? Проект почти провален.

И сказал Гугл:
2. LazyInitializationException: общие методы решения проблемы
- Не создавайте ленивые коллекции — используйте FetchMode.JOIN (FetchType.EAGER).
Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется сделать неленивыми. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML будет содержать данные и о поставщиках, и о покупателях сразу. - Не связывайтесь с ленивыми коллекциями — используйте @XmlTransient (конечно, в случаях, где вообще целесообразно говорить о применении этой аннотации).
Нет, этот вариант не подходит — обе коллекции (suppliers и customers) придется маркировать, как @XmlTransient. Тогда получится, что неважно, какой сервис вызывать: .../suppliers.xml или .../customers.xml — полученный XML не будет содержать данных ни о покупателях, ни о поставщиках. - Не давайте сессии закрыться, используя приемы X, Y, Z. (к примеру HibernateInterceptor или OpenSessionInViewFilter — для Spring и Hibernate3).
Нет, этот вариант не подходит. Из незакрытой сессии вытянутся ненужные данные и мы получаем подобие пункта 1. - Используйте DTO — промежуточный слой между DAO и? (в нашем случае? — сериализатор), где разрулите ситуацию.
Можно, но придется писать свое DTO для каждого конкретного случая. И вообще, использование DTO должно быть получше обосновано, ведь это своего рода антипаттерн, т.к. вызывает дублирование данных. - Пройдитесь по object graph «вручную» или с помощью средства XYZ (например, Hibernate lazy chopper, если используете Spring) и разберитесь с ленивыми коллекциями после получения объекта из DAO.
Этот вариант неплох и претендует на звание универсального, но в случае с сериализацией остается одна проблема — придется пройтись по object graph дважды: первый раз для устранения ленивых коллекций, второй раз это сделает сериализатор при сериализации.
Мы подходим к мысли, что в идеале сериализатор должен сам отсекать неинициализированные коллекции — так, так как это делает Jackson.
3. Custom JAXB AccessorFactory
Помимо прочего, гугл выдал 2 ссылки, до которых дело дошло в последнюю очередь:forum.hibernate и blogs.oracle.
Отпугивало от этих статей отсутствие решения, пригодного для Ctrl+C/Ctrl+V и излишняя перегруженость всякими ненужностями. Так что пришлось содержимое статей творчески доработать и переработать. Результат представлен ниже.
Итак, из упомянутых источников ясно, что нам нужно сделать:
- Написать свою реализацию AccessorFactory (класс этого типа используется JAXB для доступа к fields/properties объекта при marshalling/unmarshalling)
- Сказать JAXB, что он должен использовать custom-реализации AccessorFactory.
- Сказать JAXB, где эта реализация находится.
Поехали по пунктам:
... import com.sun.xml.bind.AccessorFactory; import com.sun.xml.bind.AccessorFactoryImpl; import com.sun.xml.bind.api.AccessorException; import com.sun.xml.bind.v2.runtime.reflect.Accessor; public class JAXBHibernateAccessorFactory implements AccessorFactory { // Реализация AccessorFactory уже написана - AccessorFactoryImpl. Она не содержит public // конструкторов, и отнаследоваться от нее не получится, поэтому сделаем ее делегатом // и напишем wrapper. private final AccessorFactory accessorFactory = AccessorFactoryImpl.getInstance(); // Также потребуется некая реализация Accessor. Поскольку больше она нигде не нужна, сделаем // ее в виде private inner class, чтобы не болталась по проекту. private static class JAXBHibernateAccessor<B, V> extends Accessor<B, V> { private final Accessor<B, V> accessor; public JAXBHibernateAccessor(Accessor<B, V> accessor) { super(accessor.getValueType()); this.accessor = accessor; } @Override public V get(B bean) throws AccessorException { V value = accessor.get(bean); // Вот оно! Ради этого весь сыр-бор. Если кому-то простое зануление // может показаться неправильным, он волен сделать тут все, что // захочется. Метод Hibernate.isInitialized() c одинаковым поведением // присутствует и в Hibernate3, и Hibernate4. return Hibernate.isInitialized(value) ? value : null; } @Override public void set(B bean, V value) throws AccessorException { accessor.set(bean, value); } } // Определим необходимые методы, используя делегат и inner Accessor. @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Accessor createFieldAccessor(Class bean, Field field, boolean readOnly) throws JAXBException { return new JAXBHibernateAccessor(accessorFactory.createFieldAccessor(bean, field, readOnly)); } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public Accessor createPropertyAccessor(Class bean, Method getter, Method setter) throws JAXBException { return new JAXBHibernateAccessor(accessorFactory.createPropertyAccessor(bean, getter, setter)); } }
Чтобы JAXB начал использовать custom-реализации следует JAXBContext установить специальное свойство «com.sun.xml.bind.XmlAccessorFactory» = true. (оно же JAXBRIContext.XMLACCESSORFACTORY_SUPPORT), которое включает поддержку аннотации @XmlAccessorFactory. В случае использования Spring, сделать это можно не на прямую, а при конфигурировании бина «org.springframework.oxm.jaxb.Jaxb2Marshaller» в свойстве «jaxbContextProperties».
И, наконец, указываем класс нашей реализации при помощи package-level аннотации @XmlAccessorFactory:
... @XmlAccessorFactory(JAXBHibernateAccessorFactory.class) package ru.habr.zrd.hls.domain; import com.sun.xml.bind.XmlAccessorFactory; ...
После выполнения указанных операций обратимся к нашему сервису для получения данных о компании и покупателях:

Все ок — только покупатели и нет поставщиков. Неинициализированная коллекция с поставщиками занулена нашей AccessorFactory, поэтому JAXB не пытается ее сериализовать и LazyInitializationException не возникает. Дальше можно наводить красоту — убрать суррогатные ключи из выдачи и др. Но это уже другая статья.
В конце, как и обещал, ссылка на исхoдный код рабочего примера (на Spring Web MVC) по теме статьи. В нем используется embedded H2, которая конфигурируется сама при запуске проекта, поэтому отдельной СУБД ставить не нужно. Для тех, кто использует Eclipse + STS plugin, в архиве есть отдельная версия, настроенная под Eclipse и STS.
На этом все, надеюсь, статья кому-нибудь окажется полезной.
