Статьи

XStream — XStreamely простой способ работы с данными XML в Java

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

обзор

XStream — это небольшая библиотека для сериализации данных между объектами Java и XML. Он легкий, небольшой, имеет приятный API и, что самое важное, работает с пользовательскими аннотациями и без них, которые мы не можем добавлять, когда мы не являемся владельцами классов Java.

Первый пример

Предположим, у нас есть требование загрузить конфигурацию из XML-файла:

01
02
03
04
05
06
07
08
09
10
11
12
13
<config>
    <inputFile>/Users/tomek/work/mystuff/input.csv</inputFile>
    <truststoreFile>/Users/tomek/work/mystuff/truststore.ts</truststoreFile>
    <keystoreFile>/Users/tomek/work/mystuff/CN-user.jks</keystoreFile>
 
    <!-- ssl stores passwords-->
    <truststorePassword>password</truststorePassword>
    <keystorePassword>password</keystorePassword>
 
    <!-- user credentials -->
    <user>user</user>
    <password>secret</password>
</config>

И мы хотим загрузить его в объект конфигурации:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class Configuration {
 
    private String inputFile;
    private String user;
    private String password;
 
    private String truststoreFile;
    private String keystoreFile;
    private String keystorePassword;
    private String truststorePassword;
 
    // getters, setters, etc.
}

Итак, в основном то, что мы должны сделать, это:

1
2
3
4
5
FileReader fileReader = new FileReader("config.xml");  // load our xml file 
    XStream xstream = new XStream();     // init XStream
    // define root alias so XStream knows which element and which class are equivalent
    xstream.alias("config", Configuration.class);   
    Configuration loadedConfig = (Configuration) xstream.fromXML(fileReader);

И это все, легкий peasy

Что-то более серьезное

Хорошо, но предыдущий пример очень прост, поэтому давайте сделаем что-то более сложное: настоящий XML, возвращаемый реальным WebService.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<DATA xmlns="">
    <BAN>
        <UPDATED_AT>2013-03-09</UPDATED_AT>
        <TROUBLEMAKER>
            <NAME1>JOHN</NAME1>
            <NAME2>EXAMPLE</NAME2>
            <AGE>24</AGE>
            <NUMBER>ASD123123</NUMBER>
        </TROUBLEMAKER>
    </BAN>
    <BAN>
        <UPDATED_AT>2012-03-10</UPDATED_AT>
        <TROUBLEMAKER>
            <NAME1>ANNA</NAME1>
            <NAME2>BAKER</NAME2>
            <AGE>26</AGE>
            <NUMBER>AXN567890</NUMBER>
        </TROUBLEMAKER>
    </BAN>
    <BAN>
        <UPDATED_AT>2010-12-05</UPDATED_AT>
        <TROUBLEMAKER>
            <NAME1>TOM</NAME1>
            <NAME2>MEADOW</NAME2>
            <NUMBER>SGH08945</NUMBER>
            <AGE>48</AGE>
        </TROUBLEMAKER>
    </BAN>
</DATA>

У нас есть простой список запретов, написанных на XML. Мы хотим загрузить его в коллекцию объектов Ban. Итак, давайте подготовим некоторые классы (getters / setters / toString опущены):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class Data {
    private List bans = new ArrayList();
}
 
public class Ban {
    private String dateOfUpdate;
    private Person person;
}
 
public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String documentNumber;
}

Как вы можете видеть, существует некоторое несоответствие имен и типов между классами XML и Java (например, поле name1-> firstName, dateOfUpdate — это String, а не Date), но это здесь для некоторых примеров. Поэтому цель здесь — проанализировать XML и получить объект Data с заполненной коллекцией экземпляров Ban, содержащих правильные данные. Посмотрим, как этого можно добиться.

Разбор с аннотациями

Во-первых, проще использовать аннотации. И это предлагаемый подход в ситуации, когда мы можем модифицировать классы Java, в которые будет отображаться XML.
Итак, мы имеем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@XStreamAlias("DATA") // maps DATA element in XML to this class
public class Data {
 
    // Here is something more complicated. If we have list of elements that are
    // not wrapped in a element representing a list (like we have in our XML:
    // multiple <ban> elements not wrapped inside <bans> collection,
    // we have to declare that we want to treat these elements as an implicit list
    // so they can be converted to List of objects.
    @XStreamImplicit(itemFieldName = "ban")
    private List bans = new ArrayList();
}
 
@XStreamAlias("BAN") // another mapping
public class Ban {
 
    /*
     We want to have different field names in Java classes so
     we define what element should be mapped to each field
    */
    @XStreamAlias("UPDATED_AT") //
    private String dateOfUpdate;
 
    @XStreamAlias("TROUBLEMAKER")
    private Person person;
}
 
@XStreamAlias("TROUBLEMAKER")
public class Person {
 
    @XStreamAlias("NAME1")
    private String firstName;
 
    @XStreamAlias("NAME2")
    private String lastName;
 
    @XStreamAlias("AGE") // String will be auto converted to int value
    private int age;
 
    @XStreamAlias("NUMBER")
    private String documentNumber;

И фактическая логика синтаксического анализа очень коротка:

01
02
03
04
05
06
07
08
09
10
11
12
FileReader reader = new FileReader("file.xml");  // load file
 
    XStream xstream = new XStream();
    xstream.processAnnotations(Data.class);     // inform XStream to parse annotations in Data class
    xstream.processAnnotations(Ban.class);      // and in two other classes...
    xstream.processAnnotations(Person.class);   // we use for mappings
    Data data = (Data) xstream.fromXML(reader); // parse
 
    // Print some data to console to see if results are correct
    System.out.println("Number of bans = " + data.getBans().size());
    Ban firstBan = data.getBans().get(0);
    System.out.println("First ban = " + firstBan.toString());

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

Разбор без аннотаций

Когда мы не можем обогатить наши модельные классы аннотациями, есть другое решение. Мы можем определить все детали отображения, используя методы из объекта XStream:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FileReader reader = new FileReader("file.xml");  // three first lines are easy,
    XStream xstream = new XStream();                 // same initialisation as in the
    xstream.alias("DATA", Data.class);               // basic example above
 
    xstream.alias("BAN", Ban.class);             // two more aliases to map...
    xstream.alias("TROUBLEMAKER", Person.class); // between node names and classes
 
    // We want to have different field names in Java classes so
    // we have to use aliasField(<fieldInXml>, <mappedJavaClass>, <mappedFieldInJavaClass>)
    xstream.aliasField("UPDATED_AT", Ban.class, "dateOfUpdate");
    xstream.aliasField("TROUBLEMAKER", Ban.class, "person");
 
    xstream.aliasField("NAME1", Person.class, "firstName");
    xstream.aliasField("NAME2", Person.class, "lastName");
    xstream.aliasField("AGE", Person.class, "age");  // notice here that XML will be auto-converted to int "age"
    xstream.aliasField("NUMBER", Person.class, "documentNumber");
 
    /*
     Another way to define implicit collection
    */
    xstream.addImplicitCollection(Bans.class, "bans");
 
    Data data = (Data) xstream.fromXML(reader);  // do the actual parsing
 
    // let's print results to check if data was parsed
    System.out.println("Number of bans = " + data.getBans().size());
    Ban firstBan = data.getBans().get(0);
    System.out.println("First ban = " + firstBan.toString());

Как видите, XStream позволяет легко преобразовывать более сложные XML-структуры в объекты Java, а также дает возможность настраивать результаты, используя разные имена, если это из XML не удовлетворяет нашим потребностям. Но одна вещь должна привлечь ваше внимание: мы конвертируем XML, представляющий Date, в необработанную строку, что не совсем то, что мы хотели бы получить в результате. Вот почему мы добавим конвертер, чтобы сделать некоторую работу для нас.

Использование существующего пользовательского конвертера типов

Библиотека XStream поставляется с набором встроенных преобразователей для наиболее распространенных случаев использования. Мы будем использовать DateConverter. Итак, теперь наш класс для бана выглядит так:

1
2
3
4
5
public class Ban {
 
    private Date dateOfUpdate;
    private Person person;
}

И чтобы использовать DateConverter, нам просто нужно зарегистрировать его в формате даты, который, как мы ожидаем, появится в данных XML:

1
xstream.registerConverter(new DateConverter("yyyy-MM-dd", new String[] {}));

вот и все. Теперь вместо String наш объект заполняется экземпляром Date. Круто и просто! Но как насчет классов и ситуаций, которые не охватываются существующими конвертерами? Мы могли бы написать свой собственный.

Написание собственного конвертера с нуля

Предположим, что вместо dateOfUpdate мы хотим знать, сколько дней назад было выполнено обновление:

1
2
3
4
public class Ban {
    private int daysAgo;
    private Person person;
}

Конечно, мы можем рассчитать его вручную для каждого объекта Ban, но использование конвертера, который сделает эту работу за нас, выглядит более интересным. Наш DaysAgoConverter должен реализовывать интерфейс Converter, поэтому нам нужно реализовать три метода с сигнатурами, выглядящими немного пугающе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class DaysAgoConverter implements Converter {
    @Override
    public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
    }
 
    @Override
    public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    }
 
    @Override
    public boolean canConvert(Class type) {
        return false;
    }
}

Последнее легко, так как мы конвертируем только класс Integer. Но остаются еще два метода с этими параметрами HierarchicalStreamWriter, MarshallingContext, HierarchicalStreamReader и UnmarshallingContext. К счастью, мы могли бы избежать работы с ними, используя AbstractSingleValueConverter, который защищает нас от механизмов такого низкого уровня. И теперь наш класс выглядит намного лучше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class DaysAgoConverter extends AbstractSingleValueConverter {
 
    @Override
    public boolean canConvert(Class type) {
        return type.equals(Integer.class);
    }
 
    @Override
    public Object fromString(String str) {
        return null;
    }
 
    public String toString(Object obj) {
        return null;
    }
}

Кроме того, мы должны переопределить метод toString (Object obj), определенный в AbstractSingleValueConverter, так как мы хотим хранить Date в XML, вычисляемом из Integer, а не в виде простого значения Object.toString, которое будет возвращено из стандартного toString, определенного в абстрактном родительском объекте .

Реализация

Код ниже довольно прост, но наиболее интересные строки комментируются. Я пропустил все проверки, чтобы сделать этот пример короче.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DaysAgoConverter extends AbstractSingleValueConverter {
 
    private final static String FORMAT = "yyyy-MM-dd"; // default Date format that will be used in conversion
    private final DateTime now = DateTime.now().toDateMidnight().toDateTime(); // current day at midnight
 
    public boolean canConvert(Class type) {
        return type.equals(Integer.class);     // Converter works only with Integers
    }
 
    @Override
    public Object fromString(String str) {
        SimpleDateFormat format = new SimpleDateFormat(FORMAT);
        try {
            Date date = format.parse(str);
            return Days.daysBetween(new DateTime(date), now).getDays();  // we simply calculate days between using JodaTime
        } catch (ParseException e) {
            throw new RuntimeException("Invalid date format in " + str);
        }
    }
 
    public String toString(Object obj) {
        if (obj == null) {
            return null;
        }
 
        Integer daysAgo = ((Integer) obj);
        return now.minusDays(daysAgo).toString(FORMAT); // here we subtract days from now and return formatted date string
    }
}

использование

Чтобы использовать наш специальный конвертер для определенного поля, мы должны сообщить об этом объекту XStream, используя registerLocalConverter:

1
xstream.registerLocalConverter(Ban.class, "daysAgo", new DaysAgoConverter());

Мы используем «локальный» метод, чтобы применить это преобразование только к определенному полю, а не к каждому полю Integer в файле XML. И после этого мы будем заполнять наши объекты Ban числом дней вместо Date.

Резюме

Это все, что я хотел показать вам в этом посте. Теперь у вас есть базовые знания о том, на что способен XStream и как его можно использовать для простого сопоставления данных XML с объектами Java. Если вам нужно что-то более продвинутое, проверьте официальную страницу проекта, поскольку она содержит очень хорошую документацию и примеры.