среда, 30 октября 2013 г.

Обзор библиотеки XML-сериализации Simple

Simple — полезная Java-библиотека для XML-сериализации/десериализации объектов. В частности, её можно использовать с Android.

Зачем это нужно?

Все мы любим XML и часто его разбираем. То какой-нибудь сервер нам его пришлёт, то в само приложение зашиты какие-нибудь данные в этом формате, то ещё что-нибудь. Разбор XML всегда выливается в написание кучи унылого однообразного кода вот такого вида (ну или такого). С Simple нам нужно только задать правила отображения объектной модели на узлы XML, дальше сериализатор сделает всё сам. Экономим время, делаем меньше ошибок, получаем более читаемый и сопровождаемый код.

И что нам за это будет?

Jar весит полмегабайта, а значит, увеличивает размер выходного приложения.

А ещё Simple работает через Reflection, так что производительность тоже не блещет. Один не особенно страшный, но разветвлённый XML-файл Simple разобрал в 20 раз медленнее, чем стандартный XmlPullParser.

Так что недостатки тоже есть, а насколько они критичные — решайте сами.

Подключение

В build.gradle добавляем следующие волшебные строчки в dependencies:

build.gradle

dependencies {
    
    compile('org.simpleframework:simple-xml:2.7.+') {
        exclude module: 'stax'
        exclude module: 'stax-api'
        exclude module: 'xpp3'
    }
}

Собственно, про подключение я пишу только ради этих exclude module. Они вовсе не случайны, их обязательно нужно писать, иначе будет возникать такая странная ошибка:

Gradle: Execution failed for task ':SimpleXmlDemo:dexDebug'. Could not call IncrementalTask.taskAction() on task ':SimpleXmlDemo:dexDebug'

Собираем, Gradle сразу всё скачивает, и библиотеку можно использовать.

Маппинги

Собственно, основное, что надо сделать — написать аннотации к полям объектной модели. Я не буду изображать javadoc и подробно расписывать все аннотации, а просто напишу пример и обращу внимание на некоторые вещи.

Итак, пример. Допустим, есть у нас XML такого вида:

os.xml

<os_reference>
    <desktop>
        <os>
            <title>Windows</title>
            <company>
                <name>Microsoft</name>
                <website>http://www.microsoft.com</website>
            </company>

            <version year="2000">Windows ME</version>
            <version codename="Whistler" year="2001">Windows XP</version>
            <version codename="Blackcomb" year="2009">Windows 7</version>
            <version codename="Jupiter" year="2012">Windows 8</version>
        </os>

        <os>
            <title>OS X</title>
            <company>
                <name>Apple</name>
                <website>http://www.apple.com</website>
            </company>

            <version codename="Lion" year="2011">10.7</version>
            <version codename="Mountain Lion" year="2012">10.8</version>
            <version codename="Mavericks" year="2013">10.9</version>
        </os>
    </desktop>

    <mobile>
        <os>
            <title>Android</title>
            <company>
                <name>Google</name>
                <website>http://google.com</website>
            </company>

            <version codename="Gingerbread" year="2010">2.3.x</version>
            <version codename="Ice Cream Sandwich" year="2011">4.0</version>
            <version codename="Jelly Bean" year="2012">4.1</version>
        </os>

        <os>
            <title>Windows Phone</title>
            <company>
                <name>Microsoft</name>
                <website>http://www.microsoft.com</website>
            </company>

            <version codename="Mango" year="2010">7.5</version>
            <version codename="Tango" year="2011">7.8</version>
            <version codename="Apollo" year="2012">8</version>
        </os>
    </mobile>
</os_reference>

Напишем соответствующую объектную модель с аннотациями.

OsList.java

@Root(name="operation_systems")
public class OsReference {
    @ElementList(name="desktop", inline = false)
    private List<OperatingSystem> desktopSystems;

    @ElementList(name="mobile", inline = false)
    private List<OperatingSystem> mobileSystems;
}

OperatingSystem.java

@Root(name = "os")
public class OperatingSystem {
    @Element
    private String title;

    @Path("company")
    @Element(name="name")
    private String companyName;

    @Path("company")
    @Element(name="website")
    private String companySite;

    @ElementList(inline = false)
    private List<Version> versions;
}

Version.java

@Root(name="version")
public class Version {
    @Attribute(required = false)
    private String codename;

    @Attribute
    private int year;

    @Text
    private String title;
}

Выглядит довольно логично и очевидно. Однако, обращу внимание на некоторые особенности.

Почему не во всех аннотациях задан параметр name?

Параметр name имеется почти у всех аннотаций и обозначает имя тега или атрибута. Однако, в приведённом примере мало где он указан. Дело в том, что по умолчанию Simple использует имя поля класса, а у нас они почти везде совпадают с соответствующими им именами элементов XML. Если бы мы использовали гугловые правила именования (с префиксом m), то пришлось бы указывать name везде.

Что за параметр inline в @ElementList?

Параметр inline показывает, где расположены элементы списка — прямо в текущем узле (true) или в отдельном (false). В нашем примере в OsReference имеется два списка в отдельных тегах desktop и mobile (у них стоит inline=true и указаны имена тегов). А вот список версий ОС валяется прямо в узле os, так что вполне себе inline.

Почему при разборе может возникать ошибка "Element 'yourElement' does not have a match in class your.package.YourClass at line 10"?

Вероятно, парсер нашёл в XML-документе нечто, что ему некуда замапить. Тут одно из двух:

  • То, на что парсер ругается, нужно и важно, а мы его забыли. Тогда это нам сигнал, что объектную модель необходимо срочно дописать.
  • Данный элемент избыточен и в нашей модели не нужен. Тогда следует найти класс, на который ругается парсер и в аннотации Root указать параметр strict=false

Что это за такой @Path?

Это такой хитрый способ заполучить данные из других узлов. Пишем сюда XPath, относительно которого будем получать элемент/атрибут/etc. В примере мы воспользовались этой возможностью фактически только для того, чтобы не создавать лишнего класса Company.

Где здесь что-нибудь про CDATA?

Десериализатору от CDATA ни жарко и ни холодно. А вот для сериализации бывает полезно.

Для того, чтобы указать, нужно её ставить или нет, в аннотациях @Text и @Element имеется параметр data. По умолчанию он false, но, если CDATA нужна, можно поставить ему true.

Так ли уж сильно нужен @Root?

У меня этот @Root вызывает большие сомнения. Во нашем случае можно смело его убирать, и разбор всё равно будет работать. Даже если после этого слегка поиздеваться над входным XML и, к примеру, в списке mobile вместо os написать mobile_os, а в desktop оставить os — и в этом случае десериализация сработает на ура.

Так что мне пока кажется, эта аннотация опциональная, и использовать её нужно только тогда, когда нужен strict=false или чтобы задать имя тега для сериализации.

Десериализация с Simple

А вот и весь разбор XML:

private void deserializeObject(InputStream is) {
    Serializer serializer = new Persister();
    try {
        OsReference result = serializer.read(OsReference.class, is);
        Log.d(TAG, "Deserializing OK");
    } catch (Exception e) {
        Log.e(TAG, "Error while parsing XML", e);
    }
}

По сравнению с километровыми XmlPullParser-ами весьма лаконично.

Сериализация с Simple

Обратная задача — сгенерировать XML из объектов — возникает в мобильной разработке куда реже. Но если уж возникла, мы сразу гуглим про XmlSerializer и пишем простыни кода не короче, чем для разбора.

А вот для сравнения весь код на Simple:

private void serializeObject() {
    // Создаём объект
    OperatingSystem debian = new OperatingSystem("Debian Linux", "Debian", "http://debian.org", new ArrayList<Version>() {{
        add(new Version("Lenny", 2009, "5.0"));
        add(new Version("Squeeze", 2011, "6.0"));
        add(new Version("Wheezy", 2013, "7.0"));
    }});

    // сериализуем объект
    Serializer serializer = new Persister();
    StringWriter writer = new StringWriter();

    try {
        serializer.write(debian, writer);
        Log.d(TAG, writer.toString());
    } catch (Exception e) {
        Log.e(TAG, "Error while serializing", e);
    }
}

Всё!

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

Посмотреть исходники (с бонусом)

1 комментарий:

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

Хорошая статья, спасибо =) но мне не хватило 1-ного важного пункта - порядок.
Изучал официальную доку (громоздкая)...
могли бы вы добавить в вашу статью пункт про конфигурирование порядка посcтроеной XML:
" Если вам потребуется задать порядок в вашем XML, то надо использовать аннотацию
@Order(elements = [....])
с перечислением нужного вам порядка. "

Лично я столкнулся с проблемой, что порядок полей в созданной мною модели не соблюдается при построении XML