среда, 14 ноября 2007 г.

Знакомимся: xstream - сериализуем Java-класс в XML


Существует множество способов сериализовать Java-объект в xml и, соответственно, десериализовать. В Java SDK входят Java Architecture for XML Binding (JAXB) и связка XMLEncoder - XMLDecoder. Существует так же ряд сторонних библиотек, предназначенных для решения данной задачи.

Одной из таких библиотек является xstream (скачать можно отсюда). Понравилась она мне своей простотой изучения и довольно приличными возможностями, которые при этом широко расширяемы и кастомизируемы. Впрочем, обо всем по-порядку.


Введение в xstream

На официальном сайте xstream находится официальный "2-х минутный туториал". И, действительно, первое приложение с использованием xstream можно написать за 2 минуты. Давайте попробуем. Сначала создадим класс, который мы будем сериализовать:

package org.beq.xstream.demo;

/**

* Класс, который будет сериализован в XStream

*

* @author Pavel

*

*/


public class SerializableClass {

    public static final String cons = "100";

   

    public static final int coni = 100;



    private int intfield;

   

    private double doublefield;

 

    public SerializableClass(int intfield, double doublefield) {

        this.intfield = intfield;

        this.doublefield = doublefield;        

    }



    public static int getConi() {

        return coni;

    }



    public static String getCons() {

        return cons;

    }

 

    public double getDoublefield() {

        return doublefield;

    }



    public int getIntfield() {

        return intfield;

    }

}


Как видим, класс содержит 2 константы, переменную типа int, переменную типа double и гетеры, которые пригодятся при демонтрации механизм сериализации.

Теперь займемся классом, который будет демонстрировать механизм сериализации /десериализации с помощью xstream.


package org.beq.xstream.demo;



import com.thoughtworks.xstream.XStream;



/**

* Демонстрируем сериализацию в xml и десериализацию оттуда

*

* @author Pavel

*

*/


public class Demo {



   /**

    * @param args

    */


   public static void main(String[] args) {

      XStream xstream = new XStream(); // require XPP3 library

      xstream.alias("serializableclass",  SerializableClass.class);

     

      // Serialize

      SerializableClass sclassOut = new SerializableClass(100, 100.0);

      String xml = xstream.toXML(sclassOut);

      System.out.println(xml);

 

      // Deserialize

      SerializableClass sclassIn = (SerializableClass) xstream.fromXML(xml);

      System.out.println("sclassOut == sclassIn: " + (sclassOut == sclassIn));

      System.out.println("sclassOut.intfield == sclassIn.intfield: " + (sclassOut.getIntfield() == sclassIn.getIntfield()));

      System.out.println("sclassOut.doublefield == sclassIn.doublefield: " + (sclassOut.getDoublefield() == sclassIn.getDoublefield()));

   }

}


Разберем метод main подробнее. Сначала создается экземпляр потока xstream. Конструктор класса XStream в качестве параметра принимает объект, через который будет происходить обработка XML. По умолчанию это - библиотека XPP3.

Далее создаем алиас для класса SerializableClass. Если не создать алиас, то содержимое класса (его поля) будет помещено в тег org.beq.xstream.demo.SerializableClass, что не красиво. Про алиасы мы еще поговорим подробнее.

Следующий код:


// Serialize

SerializableClass sclassOut = new SerializableClass(100, 100.0);

String xml = xstream.toXML(sclassOut);

System.out.println(xml);



Здесь мы создаем экземпляр класса, который будем сериализовать. После чего сериализуем его в XML-строку и выводим ее пользователю. Ничего сложного здесь нет.

Десериализация осуществляется следующим образом:


// Deserialize

SerializableClass sclassIn = (SerializableClass) xstream.fromXML(xml);



Я думаю, что комментировать здесь нечего. Теперь надо сравнить поля объекта до сериализации и после десериализации:


System.out.println("sclassOut == sclassIn: " + (sclassOut == sclassIn));

System.out.println("sclassOut.intfield == sclassIn.intfield: " + (sclassOut.getIntfield() == sclassIn.getIntfield()));

System.out.println("sclassOut.doublefield == sclassIn.doublefield: " + (sclassOut.getDoublefield() == sclassIn.getDoublefield()));



Первое сравнение всегда будет false - объект, записаный в стрим и прочитанный оттуда - это разные объекты. Вторая и третья проверки должны вернуть true.

Возможности и особенности xstream

Сериализация с помощью xstream обладает рядом интересных особенностей и возможностей:

  • Сериализуемые классы не обязаны реализовывать интерфейс Serializable.
  • Для получения списка полей класса и значений этих полей используется Reflection API.
  • Константы не сериализуются.
  • Поля с модификатором transient не сериализуются.
  • Коллекции и связанные классы сериализуются прозрачно. Никаких дополнительных действий предпринимать не надо.
  • Формирование XML возможно через несколько поддерживаемых библиотек. Так же возможна сериализация в JSON.
  • Вид результирующего XML удобно настраивается как с помощью методов (удобно при использование JDK до 1.5), так и с помощью аннотаций.
  • Можно самому управлять процессом генерации XML (маршаллинг). Программист имеет возможность самостоятельно описывать процедуры сериализации/десериализации для того или иного класса.


Настройка структуры XML-документа

Главным преимуществом xstream является возможность настроить структуру XML-документа, получаемого в результате сериализации. Так же данная возможность очень удобна, если мы десериализуем чужой XML-документ. Существует 2 основных способа настройки: вызов соответствующих методов класса XStream и использование аннотаций в сериализуемом классе. Давайте рассмотрим каждый способ подробнее.

Настройка вызовом методов.

Способ имеет ряд недостатков, но является единственно возможным при использовании JDK версий ниже 5-й. Суть заключается в том, что класс XStream имеет ряд методов для управления так называемыми алиасами. Можно создать алиас к классу, убирать те или иные теги, заставлять поля сериализоваться в атрибуты XML-документа (по умолчанию сериализация ведется в теги).

Расмотрим основные алиас-методы. Описание остальных всегда можно найти в JavaDoc.


xstream.alias("email", Email.class);



Метод alias создает алиас для класса. По умолчанию класс сериализуется в тег, имя которого равно полному имени сериализуемого класса, например org.beq.xstream.collection.entity.Email, что не добавляет красоты и читабельности XML-документу. После алиаса же класс будет сериализоваться в теги email. Можно также создавать алиасы для тех или иных полей класса, но это менее критично.

По умолчанию поля, которые являются коллекциями, обрамляются тегом с названием поля, например <emails>. Это не всегда удобно, иногда хочется чтобы элементы коллекции находились непосредственно в рут-ноде XML-документа. Убрать обрамляющий тег можно с помощью:


xstream.addImplicitCollection(Person.class, "emails");



Собственно, данную команду можно применять к любому полю объектного типа.

Ну и наиболее интересная возможность - сериализация полей в атрибуты XML-документа. Чтобы то или иное поле сериализовалось атрибутом, достаточно вызвать следующую команду:


xstream.useAttributeFor(Person.class, "name");


Управление сериализацией с помощью аннотаций

Вторым и на мой взгляд самым удобным способом управления сериализацией является использование аннотаций в сериализуемом классе. Действительно, гораздо удобнее, когда инструкции о том, как сериализовать класс, находятся в самом классе, а не у сериализатора и десериализатора. Во-первых, такой код проще читать и понимать, а во-вторых становится ненужным дублирование кода. Давайте рассмотрим, как с помощью аннотаций управлять процессом:


package org.beq.xstream.annotation.entity;



import java.util.List;



import com.thoughtworks.xstream.annotations.XStreamAlias;

import com.thoughtworks.xstream.annotations.XStreamAsAttribute;

import com.thoughtworks.xstream.annotations.XStreamImplicit;



/**

* Класс - персона. Используем анотации, чтобы разобраться как его сериализовать

*

* @author Pavel

*

*/


@XStreamAlias("person")

public class Person {



   @XStreamAsAtribute

   private Long id;



   @XStreamAsAttribute

   private String name;



   @XStreamAsAttribute

   @XStreamAlias("family")

   private String sername;



   @XStreamImplicit(itemFieldName="e-mail")

   private List emails;



   public Person() {

   }



   public Person(Long id, String name, String sername) {

       this.id = id;

       this.name = name;

       this.sername = sername;

   }



   public List getEmails() {

       return emails;

   }



   public void setEmails(List emails) {

       this.emails = emails;

   }



   public Long getId() {

       return id;

   }



   public void setId(Long id) {

       this.id = id;

   }



   public String getName() {

       return name;

   }



   public void setName(String name) {

       this.name = name;

   }



   public String getSername() {

       return sername;

   }



   public void setSername(String sername) {

       this.sername = sername;

   }

}



Логично, что все аннотации xstream начинаются с префикса @XStream. Используемые в приведенном фрагменте аннотации обозначают следующее:

  • @XStreamAlias("person") - аналогично методу alias. Теперь класс Person будет сериализоваться в XML-тег person.
  • @XStreamAsAttribute - как видно из названия, указывает сериализовать поле не в XML-тег, а в XML-атрибут. В данном случае тега person.
  • @XStreamImplicit(itemFieldName="e-mail") - убираем обрамляющий тег emails. Теперь каждый элемент коллекции emails будет сериализовываться в тег e-mail.

О других аннотациях можно прочитать в документации.

Но, чтобы мета-данные, заданные с помощью аннотаций, применились, необходимо при сераилизации/десериализации вызвать методы:


// Конфигурим аннотациями

Annotations.configureAliases(xstream, Person.class);

Annotations.configureAliases(xstream, Email.class);


где xstream - экземпляр класса XStream, через который будет осуществляться работа.

Так, с конфигурированием закончили. Рассмотрим еще довольно интересную тему - сериализацию в JSON.

Сериализация в JSON

JSON - Java Script Object Notattion - формат, который как и XML-очень удобен для определения иерархических структур, в частности объектов. Особенно популярен такой формат для осуществления AJAX-взаимодействия. А т.к. он является нативным для JavaScript - работать с ним на клиентской стороне гораздо проще, чем с XML.

Библиотека xstream поддерживает сериализацию Java-объектов в JSON. Для работы с данным форматом существует два драйвера: JsonHierarchicalStreamDriver
и JettisonMappedXmlDriver. Первый драйвер похуже, т.к. во-первых не включен в базовую поставку xstream, а во-вторых - не поддерживает десериализацию из JSON.

Второй драйвер включен в поставку xstream и поддерживает, как сериализацию, так и десериализацию. Самое главное - чтобы работать с JSON вместо XML - достаточно в конструкторе XStream передать параметром экзепляр драйвера.


XStream xstream = new XStream(new JettisonMappedXmlDriver());


И ву-а-ля, теперь весь вывод идет в формат JSON, например в такие строки:


{"product":{"name":"Banana","id":"123","price":"23.0"}}


Остальная работа полностью аналогична сериализации-десериализации в XML.

Сериализация/десериализация объектов через потоки ввода-вывода

UPD: В комментариях задали вопрос - как сериализовать коллекции объектов с помощью XStream. Я решил ответить на этот довольно интересный вопрос прямо в посте

Для сериализации/десериализации коллекций с помощью XStream можно использовать стандартные средства java.io: ObjectOutputStream и ObjectInputStream соответственно.

Давайте рассмотрим пример сериализации/десериализации коллекции объектов, которые в свою очередь тоже содержат коллекции. Этакий стресс-тест. Думаю, что для получения максимального представления о работе XStream с коллекциями, не стоит производить настройку сериализации.

Итак, сначала код сериализуемого класса:

package org.beq.xstream.demo;



import java.util.ArrayList;

import java.util.Collection;



public class MyClass {



    private Collection<Double> _doubles = new ArrayList<Double>();



    public MyClass() {}



    public MyClass(Collection<Double> doubles)

    {

        _doubles = doubles;

    }



    public void addDouble(Double doubl)

    {

        _doubles.add(doubl);

    }



    @Override

    public String toString()

    {

        return "MyClass [_doubles=" + _doubles + "]";

    }

}

 


Здесь нет ничего сложного - обычный POJO, в котором мы переопределили метод toString, чтобы потом было легко посмотреть результат десериализации.

Гораздо интереснее, как мы будем работать с этим классом:

package org.beq.xstream.demo;



import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.EOFException;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.OutputStreamWriter;

import java.util.ArrayList;

import java.util.Collection;

import java.util.List;



import com.thoughtworks.xstream.XStream;

import com.thoughtworks.xstream.io.xml.Dom4JDriver;



public class Main

{

    public static void main(String[] args)

    {

        try

        {

            XStream xoutstream = new XStream();

            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("myfile.xml")));

            ObjectOutputStream out = xoutstream.createObjectOutputStream(writer, "rootnode");



            // Create collection

            List<MyClass> lst = new ArrayList<MyClass>();

            for (int i = 0; i < 10; i++)

                lst.add(new MyClass(generateCollection()));



            // Serialize collection

            for (MyClass obj: lst)

                out.writeObject(obj);



            out.close();



            XStream xinstream = new XStream(new Dom4JDriver());

            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("myfile.xml")));

            ObjectInputStream in = xinstream.createObjectInputStream(reader);



            try

            {

                while (true)

                {

                    MyClass obj = (MyClass) in.readObject();

                    System.out.println(obj);

                }

            }

            catch (EOFException e)

            {

                System.out.println("Everythings readed!");

            }

        }

        catch (FileNotFoundException e)

        {

            System.out.println("could not open file: myfile.xml");

        }

        catch (IOException e)

        {

            System.out.println("could not write to file: myfile.xml");

        }

        catch (ClassNotFoundException e)

        {

            System.out.println("Class not found");

        }

    }



    private static Collection<Double> generateCollection()

    {

        Collection<Double> result = new ArrayList<Double>();

        for (int i = 0; i < 10; i++)

            result.add(Math.random());



        return result;

    }

}

 


Прежде всего следует понять, зачем используется два объекта класса XStream. Дело в том, что сериализовать через ObjectOutputStream XStream может и через стандартный драйвер, а вот десериализовать - увы. Чтобы десериализовать коллекцию объектов необходимо использовать драйвер, основаный на DOM, например Dom4JDriver. Кстати, не забудьте подключить сам Dom4J.

Далее необходимо помнить, что поток на запись необходимо закрыть. Иначе данные будут кэшироваться, но в файл могут не записаться. Впрочем, это относится не столько к XStream, сколько к общему пониманию процессов ввода-вывода.

Важный вопрос, который возникает при десериализации: как определить, что поток прочитан полностью и больше объектов нет? Очень просто - при попытке чтения из пустого потока будет брошен EOFException, который можно перехватить и надлежащим образом обработать.

Заключение

Вот собственно и все, что я хотел рассказать о библиотеке xstream. Несмотря на довольно большие возможности, библиотека необычайно мощна, гибко настраиваема и проста для изучения. Лично у меня ушло всего 3 часа, чтобы освоить ее базовые возможности, которые я перечислил в данной статье. За кадром осталось написание своих ValueConverter'ов, реализация маршалинга-демаршалинга, кастомизация и многое другое. Но данные темы можно найти на сайте библиотеки, а ответы на многие вопросы в faq.

Понравилось сообщение - подпишись на блог через RSS

9 комментариев:

Сергей комментирует...

5 баллов... отличная статья... спасибо...

Анонимный комментирует...

А как быть с коллекциями? Что-то я ничего не нашел по этому поводу. Стоит задача сериализовать в xml коллекцию данных, полученных через hibernate. Каждый элемент коллеции в себе содержит еще коллекцию. Такую задачу можно решить с помощью xstream или же смотреть в сторону jaxb? Спасибо.

Samolisov Pavel комментирует...

to Анонимный

Ответил в посте, как сериализовать/десериализовать коллекции объектов через потоки ввода/вывода. Хотя этот способ может показаться излишне громоздким.

Так же можно вспомнить, что коллекция - это ничто иное, как объект некоторого класса, например ArrayList. Никто не мешает сериализовать ее как любой другой объект. Единственно, у нас обычно нет возможности править исходники этого класса, поэтому настройку схемы xml придется делать вызовом соответствующих методов (см. пост)

Анонимный комментирует...

Почему на блоге так мало тем про кризис, Вас этот вопрос не волнует?

Pavel Samolisov комментирует...

А почему он должен меня волновать?

phoenix_1 комментирует...

Вечер добрый.
А как быть с полями в xml которые мы не хотим обрабатывать? Вот в файле они есть, а в классе их нет и не нужны он нам, например.

Pavel Samolisov комментирует...

Здесь я затрудняюсь ответить - никогда не стояло такой задачи.

phoenix_1 комментирует...

Одно из решений проблемы, которую я указал выше
XStream xstream = new XStream(new DomDriver()) {
@Override
protected MapperWrapper wrapMapper(final MapperWrapper next) {
return new MapperWrapper(next) {
@Override
public boolean shouldSerializeMember(Class definedIn,
String fieldName) {
if (definedIn == Object.class) {
return false;
}
return super.shouldSerializeMember(definedIn, fieldName);
}
};
}
};

phoenix_1 комментирует...

Второй вариант решения - это надо химичить с конвертерами

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

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!