Для сериализации объектов достаточно имплиментировать интерфейс Serializable и после этого одним вызовом метода writeObject класса ObjectOutputStream сохранять объекты в любой поток. Этот поток может быть массивом на памяти, файлом или сокетом. Создав правильным образом экземпляр класса ObjectOutputStream можно заливать данные в любых направлениях.
Однако я работал не с совсем стандартными объектами и их сериализация несколько усложнилась. В мою задачу входила серилизация объектов полученных из базы данных через Hibernate библиотеку и передача их на другую машину в сети. Теперь я хочу поделиться найденым решением для удобной сериализации Hibernate объектов.
Работая с базой данных через Hibernate у вас есть выбор. Этот выбор напрямую скажется на том как эффективно и как сложно будет реализовавать мехнизм передачи объектов.
Первый вариант заключается в том, что бы получать все объекты полностью загруженными из базы данных. Это довольно простой способ, все данные загружены в объект и сериализация объектов сводится к простому помещению объектов в поток без подготовки. Однако, если вы начнете работать с базой данных в таком режиме - то это будет крайне не эффективно и врядли ваш сервер сможет рабоать больше чем с 10 пользователями одновременно.
Второй вариант позволяет получаеть объект не полностью инициализированным данными. Он заключается в том, что бы после получения объекта данные в нем подгружались из базы только после обращения к ним. То есть например, вы получаете объект записи транзакций за последние 3 месяца и обращаетесь к запясям начиная с 5000 номера по 9000 номер. В этом случае Хибернейт получит из базы только указанный интервал данных, без выкачивания всех остальных записей. Ко всему прочьему, так же не будут сразу загружены все связанные объекты и свойства классов. Работа по такой схеме очень ускоряется так как объем трафика и данных получаемой с базы уменьшается.
Однако если вы попытаетесь передать по сети полученный объект с без загруженных объектов то ява выдаст следующую ошибку:
org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:167)
org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:215)
org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:190)
com.payment.dao.Wallet_$$_javassist_2.getId(Wallet_$$_javassist_2.java)
Это происходит по одной причине. Объект загруженный с базы данных, переданный по сети передается вместе со всеми данными и внутренними структурами содержащие ссылки на текущее соединение с базой данных. Но так как вы передали объект на другую машину и попытались получить данные которые не были загружены - выдается ошибка обозначающая отсутствие связи с базой данных.
Что бы этого избежать необходимо специальным образом обработать объект так, что бы все данные в него загрузились до отправки его на другой комьютер. Для этого существуют разные способы и самый правильный будет использовать библиотеку Dozer.
Эта библиотека позволяет преобразоваывать объекты на лету полученных из базы данных из одного типа, набора полей и иерархии в другой набор объектов который вы хотели бы получить на выходе. Эта библиотека программируется через xml конфигурационный файл и позволяет задавать преобазования настариваемые пользователем. Полезным для нас свойством этой библиотеки помимо преобразования объектов будет копирование объектов на лету. Так что поместив не полностью загруженный объект в метод mapper на выходе из него мы получем копию, полностью загруженных данных. После чего мы без труда отправим копию по сети изключив из нее всю информацию о сесиях и базы данных. В нашем же случае, нам даже не придется делать дополнительные настройки. В самом простом случае, Dozer можно запустить без настроек и тогда она будет преобразоыввать объект из самого себя и сам себя.
Поэтому для передачи объектов нам не потребуется разбераться в настройках, а будет достаточно просто вызова метода mapper.
Вот такой простой код позволяет получать на вход объект с сессией без данных, а на выходе получать объект с полностью загруженными данными и без информации о обазе данных и сессии:
@SuppressWarnings("unchecked")
static public <T> T dozer(T o) {
if (o == null)
return null;
Mapper mapper = DozerBeanMapperSingletonWrapper.getInstance();
return (T) mapper.map(o, o.getClass());
}
Однако на практике все оказалось не так хорошо. Из за ошибки в библиотеки эта конструкция не работала. Программа валилась где-то внутри ее собственного кода и не позволяла сериализовавть объект на выходе этой функции.
Я привожу этот пример, как потенциально рабочий и возможно с ним не будет проблем в следующем резиле программы. На момент написания статьи он не работал. Версия Dozer: 5.3.2. javassist 3.12.1.GA.
SEVERE: Servlet.service() for servlet server threw exception
java.lang.NullPointerException
at javassist.util.proxy.RuntimeSupport$DefaultMethodHandler.invoke(RuntimeSupport.java:37)
at com.payment.dao.User_$$_javassist_13.writeReplace(User_$$_javassist_13.java)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:616)
at java.io.ObjectStreamClass.invokeWriteReplace(ObjectStreamClass.java:1049)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1131)
Что бы обойти сложившуюся ситуацию есть несколко вариантов. Приведу только один.
Для передачи объекта необходимо форсировать загрузку данных для Lazy объекта. Для этого необходимо вызвать метод Hibernate.initialize(object) для всех объектов полученных с через hibernate.
Скажем если вы загружаете объект командой:
static public Wallet get(Long id) {
return (Wallet) sessionFactory.getCurrentSession().createQuery("from Wallet where id = :id")
.setParameter("id", id).uniqueResult();
}
Новый объект Wallet w; должен быть проинициализирован вызовом Hibernate.initialize(w) После чего его можно сериализовать для передачи по сети. Однако подгружены будут только простые переменные класса. И если в вашем классе есть связанные коллекции ссылающиеся на другие объекты базы данных то они не будут загружены и вы не сможете сохранить объект в поток.
Для того что бы инициировать все связанные объекты необхоимо вручную вызвать методы для этих коллекций и связанных объектов. Так например если ваш объект Кошелек показывает список Транзакций, которые в свою очередь могут ссылаться на другие объекты необходимо вызвать все методы поочереди. Wallet w; for (Transaction t : w.getTransactions()) t.getUser(); и так далее.
С проктической точки зрения необходимо автоматизировать этот процесс через работу с java reflect api. Моя реализация, которая решает данную проблему можеть быть более элегантной и не использовать обязательного наследования. Но я хочу привести ее в том виде который я использовал.
Для сериализации всех связанных объектов @Entity необходимо их унаследовать от одного класса Lazy. Затем у верхнего объекта, который со всеми вложенными данными необходимо вызвать метод lazyInit()
public class Lazy {
boolean lazyLoaded = false;
boolean lazyTest(Class<?> cc, Class<?> ccc) {
try {
return cc.asSubclass(ccc) != null;
} catch (ClassCastException e) {
return false;
}
}
boolean lazyGeneric(Type c, Class<?> ccc) {
try {
ParameterizedType pp = (ParameterizedType) c;
Class<?> tt = (Class<?>) pp.getActualTypeArguments()[0];
return tt.asSubclass(Lazy.class) != null;
} catch (ClassCastException e) {
return false;
}
}
public Object[] lazyAnnotate() {
try {
ArrayList<Object> list = new ArrayList<Object>();
Class<?> c = this.getClass();
for (Field f : c.getDeclaredFields()) {
Class<?> cc = f.getType();
Type g = f.getGenericType();
f.setAccessible(true);
if (lazyTest(cc, Lazy.class))
list.add(f.get(this));
if (lazyGeneric(g, Lazy.class))
list.add(f.get(this));
}
return list.toArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
public void lazyInit() {
if (lazyLoaded)
return;
lazyLoaded = true;
for (Object o : lazyAnnotate()) {
if (o instanceof Lazy) {
((Lazy) o).lazyInit();
}
if (o instanceof Set<?>) {
for (Lazy l : (Set<? extends Lazy>) o) {
l.lazyInit();
}
}
if (o instanceof List<?>) {
for (Lazy l : (List<? extends Lazy>) o) {
l.lazyInit();
}
}
}
Hibernate.initialize(this);
}
}
Метод вызываемый перед сериализацией:
static public <T> T dozer(T o) {
if (o == null)
return null;
((Lazy) o).lazyInit();
return o;
}
@SuppressWarnings("unchecked")
static public <T> List<T> dozer(List<T> e) {
if (e == null)
return null;
ArrayList<T> r = new ArrayList<T>();
for (Object o : e) {
r.add((T) Dozer.dozer(o));
}
return r;
}
Наследование всех классов Entity, которые должны быть инициализированы:
@Table(name = "wallet")
public class Wallet extends Lazy implements Serializable {


0 коммент.:
Отправить комментарий