DE   |   EN   |   RU

Келтенринг 17, D – 82041 Оберхахинг Мюнхен  |  Тел.: +49 89 4554 6533  |  E-Mail: info@intechcore.com 

Применение JAXB для XML-сериализации объектной модели с иерархией наследования.

В мире Java библиотека JAXB уже давно (как сообщает нам wikipedia с 2006 года) и по праву является распространенным и очень удобным инструментом для XML-cериализации объектов и даже объектных моделей. В интернете есть масса примеров и целые сторонние библиотеки, построенные на JAXB — взять хотя бы docx4j (http://www.docx4java.org/trac/docx4j), которая работает с моделью документа в Open-XML формате. Так же много информации можно почерпнуть у самих авторов JAXB — https://jaxb.java.net/. Без труда найдутся в интернете прекрасные готовые исходники для генерации классов Java по XML-модели. Примеров масса и можно увлекательно провести время, изучая возможности JAXB.

Но практически все примеры приводятся для одного простого класса, который сериализуют в XML, что называется, с нуля. А что делать, если уже есть некая готовая объектная модель со сложной структурой наследования и зависимостей, которые запрещены к изменениям? Где следует применить JAXB-аннотации? Куда их добавить — непосредственно в каждый из доброй полторы сотни классов, либо в один базовый? Если в каждый класс иерархии, то целесообразна ли такая переделка с точки зрения объема выполненной работы? Если в один-два базовых, то будет ли вообще работать XML-сериализация? Да, действительно много вопросов, и посмотрим, как JAXB с этим справляется для конкретной задачи проекта.

Например, есть модель документа: Root – некий корневой элемент, контейнер для других элементов. Он может содержать Paragraph, который также является контейнером для элементов Text. В свою очередь все элементы являются контейнерами атрибутов, например у Paragraph это Alignment и Indent, у Text – Bold, Italic, FontName, FontSize и Color. Конечно, это неполный перечень, но в качестве примера достаточный. Кроме того, условием для контейнеров элементов – Root и Paragraph, является то, что они могут содержать неограниченное количество подэлементов, т.е. Root содержит много Paragraph, а Paragraph — много Text. Но контейнеры атрибутов могут содержать каждый атрибут в единственном экземпляре. Например, зачем элементу Text несколько FontName или Bold?
Таким образом, есть уже готовая модель элементов и атрибутов, со своей специфической бизнес-логикой, полями, методами и т.д.

//Начнем с базового абстрактного атрибута:

public abstract class Attribute<T> {
    protected T value;

    public Attribute(){
        this(null);
    }
    public Attribute(T value){
        this.value = value;
    }
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}

//Его наследники для TextElement:


public class BoldAttribute extends Attribute<Boolean> {
    public BoldAttribute(){
        super(Boolean.FALSE);
    }
    public BoldAttribute(Boolean value){
        super(value);
    }
}
 
public class ItalicAttribute extends Attribute<Boolean> {
    public ItalicAttribute(){
        super(Boolean.FALSE);
    }
    public ItalicAttribute(Boolean value){
        super(value);
    }
}
 
public class FontNameAttribute extends Attribute<String> {
    public FontNameAttribute (){
        super("");
    }
    public FontNameAttribute (String value){
        super(value);
    }
}
 
public class FontSizeAttribute extends Attribute<Integer> {
    public FontSizeAttribute (){
        super(10);
    }
    public FontSizeAttribute (Integer value){
        super(value);
    }
}
 
public class ColorAttribute extends Attribute<java.awt.Color> {
    public ColorAttribute(){
        super(null);
    }
    public ColorAttribute(java.awt.Color value){
        super(value);
    }
} 

//Наследники Attribute для ParagraphElement:

public class AlignmentAttribute extends Attribute<AlignmentAttribute.VALUE> {
    public enum VALUE {
        NONE, LEFT, RIGHT, CENTER
    }
    public AlignmentAttribute() {
        super(VALUE.NONE);
    }
    public AlignmentAttribute(VALUE value) {
        super(value);
    }
}
 
public class IndentAttribute extends Attribute<Integer> {
    public IndentAttribute(){
        super(0);
    }
    public IndentAttribute(Integer value){
        super(value);
    }
}

//Теперь абстрактный терминальный элемент – контейнер атрибутов:

public abstract class TerminalElement {
    public TerminalElement(){
    }
    protected Map<Class, Attribute> attributes = new LinkedHashMap<Class, Attribute>();
    public List<Attribute> getAttributes() {
        return new ArrayList<Attribute>(attributes.values());
    }
    public void addAttribute(Attribute attribute){
        if (attribute == null){
            return;
        }
        attributes.put(attribute.getClass(), attribute);
    }
}

//Как видно, набор атрибутов содержит контейнер LinkedHashMap, который позволяет поддержать уникальность атрибутов и сохранить их последовательность по мере добавления.
//Простым наследником этого класса является Text:

public class TextElement extends TerminalElement {
    private String text;
    public TextElement(String text){
        this.text = text;
    }
    public TextElement(){
        this(null);
    }
    public String getText(){
        return text;
    }
    public void setText(String text){
        this.text = text;
    }
}

//Далее имеем абстракцию контейнера элементов:
public abstract class BranchElement extends TerminalElement {
    public BranchElement(){
    }
    protected List<TerminalElement> elements = new ArrayList<TerminalElement>();
    public void addElement(TerminalElement element){
        elements.add(element);
    }
    public List<TerminalElement> getElements(){
        return elements;
    }
}

//Как уже указывалось, контейнер элементов не ограничен в их количестве по типу, поэтому тут простой ArrayList.
//Ну и наследники абстрактного контейнера очень просты:
public class ParagraphElement extends BranchElement {
    //
}
public class RootElement extends BranchElement{
    //
}

Это все, хотя реальная модель содержит гораздо больше элементов, атрибутов, ограничения по содержимому: например, Root может содержать только Paragraph, в свою очередь Paragraph – только Text, Text – только текстовые атрибуты, Paragraph – свои, Root – свои. Сейчас же в этом примере можно сделать все, что угодно и заполнить модель ошибочными данными, но речь тут о JAXB, поэтому перейдем уже к XML-cериализации этой модели, ее трудностям и их решениям.
Чтобы создать тестовую модель в памяти воспользуемся методом-стартером main(), например в RootElement:

public static void main(String[] args) throws Exception {
        final RootElement re = new RootElement();
        final ParagraphElement pe = new ParagraphElement();
        pe.addAttribute(new IndentAttribute(1000));
        pe.addAttribute(new AlignmentAttribute(AlignmentAttribute.VALUE.CENTER));
        final TextElement te = new TextElement("test!!!");
        pe.addElement(te);
        re.addElement(pe);
        te.addAttribute(new BoldAttribute(Boolean.TRUE));
        te.addAttribute(new ItalicAttribute(Boolean.TRUE));
        te.addAttribute(new ColorAttribute(java.awt.Color.RED));
        te.addAttribute(new FontName("Arial"));
        te.addAttribute(new FontSize(14));
    }

Вот теперь хотелось бы иметь возможность сохранить эту структуру в XML-файл и зачитать обратно. Причем надо это сделать так, чтобы не были затронуты уже используемые в бизнес-логике механизмы – т.е. готовые поля и методы должны остаться нетронутыми, и мы применим JAXB только чтобы незаметно для внешнего мира расширить возможности модели сохранять/читать себя в/из XML-файла.

JAXB работает через аннотации: @XmlElement, @XmlType и т.д. Классы с такими аннотациями через механизм рефлекшена проходят так называемый маппинг, и происходит магия связывания объекта и его полей с XML.

Начнем с того, что корневой элемент надо обозначить аннотацией @XmlRootElement.

В данном случае, очевидно, что это сам RootElement:

@XmlRootElement(name = "RootElement")
public class RootElement extends BranchElement {
…

Атрибут name используется для того, чтобы определить, как будет назван элемент в XML-файле.
Добавим в абстрактный атрибут специальные методы:

@XmlElement(name = "Value")
protected T getXmlValue(){
    return value;
}
protected void setXmlValue(T value){
    this.value = value;
}

Название метода getXmlValue не играет роли, главное, что мы делаем его protected и он невидим извне и не влияет на существующую бизнес-логику, а @XmlElement сообщает JAXB, что в XML у атрибута должен быть элемент с именем name = «Value», в котором будет value атрибута. Причем для JAXB важно иметь пару getter-setter, чтобы он мог сохранять/читать XML.

Да, конечно, можно сделать проще и без getter-setter – повесить @XmlElement прямо на value атрибута:

@XmlElement(name = "Value")
protected T value;

Но мы договорились не трогать содержимое модели, а ее просто чуть-чуть и незаметно расширить.
Это все? Нет! Сам класс абстрактного атрибута нужно обозначить так:

@XmlAccessorType(XmlAccessType.NONE)
@XmlTransient
public abstract class Attribute<T> {
…

@XmlAccessorType(XmlAccessType.NONE) позволит избежать маппинга по умолчанию, когда все найденные поля, пары getter-setter и т.д. будут принудительно пытаться записаться в XML.
@XmlTransient позволит в свою очередь избежать так называемой коллизии имен. Возникает такая коллизия из-за value – это и поле класса, и элемент в XML @XmlElement(name = «Value»), причем для JAXB регистр не имеет значения – value и Value одинаковы в XML. Чтобы точно сказать JAXB, что за Value пойдет в XML, мы и используем комбинацию @XmlAccessorType(XmlAccessType.NONE) и @XmlTransient – таким образом класс «закрывается» от JAXB, и открывает только getter-setter через @XmlElement(name = «Value»).
Конечно, напрашивается подозрения в искусственности проблемы – что мешает назвать @XmlElement(name = «MySuperPuperValue») и избежать @XmlTransient? Да ничто, просто мне приятнее видеть в XML «Value».
Идем дальше – что нам надо теперь сделать с атрибутами-наследниками? Да практически ничего! Атрибуты, у которых value – простые типы (boolean, int …) или String не требуют вообще никаких доработок. JAXB сериализует их в XML автоматически. Это не может не радовать, т.к. таких атрибутов в реальной модели действительно очень много и подобная полная переработка вызвала бы сомнения в целесообразности затрат.
В данном случае оставим без изменений BoldAttribute, ItalicAttribute, FontName и FontSize у текста, и IndentAttribute у параграфа.
А что с ColorAttribute? Вот он как раз не простой тип, а java.awt.Color, поэтому требует конвертации в некий простой. Для этого JAXB предлагает абстракцию XmlAdapter. Приведу тут простую реализацию:

public static class ColorAdapter extends XmlAdapter<Integer, java.awt.Color> {
        @Override
        public java.awt.Color unmarshal(Integer v) throws Exception {
            if (v == null){
                return null;
            }
            return new Color(v);
        }
 
        @Override
        public Integer marshal(java.awt.Color v) throws Exception {
            if(v == null) {
                return null;
            }
            return v.getRGB();
        }
}

Что делает этот адаптер? Получает java.awt.Color и преобразует его в число из его RGB значения.
Переопределим в ColorAttribute @XmlElement:

@XmlJavaTypeAdapter(ColorAdapter.class)
@XmlElement(name = "Value")
@Override
protected java.awt.Color getXmlValue(){
    return value;
}
 
@Override
protected void setXmlValue(java.awt.Color value){
    this.value = value;
}

Теперь JAXB сможет сохранить java.awt.Color.RED из примера в main() так:
-65536.
Да, есть еще замечание по java.awt.Color – в реальной модели я использовал другой адаптер, т.к. color.getRGB() теряет alpha-составляющую цвета и пришлось хранить цвет в XML в специальной строке RGBA:

protected static class ColorAdapter extends XmlAdapter<String, Color> {
        private final static Pattern REGEX_RGBA = Pattern.compile("rgba\\((\\d+),(\\d+),(\\d+),(\\d+)\\)");
 
        @Override
        public java.awt.Color unmarshal(String v) throws Exception {
            if (v == null || v.length() == 0) {
                return null;
            }
            final Matcher m = REGEX_RGBA.matcher(v);
            if (m.find() && m.groupCount() == 4) {
                final int r = Integer.valueOf(m.group(1));
                final int g = Integer.valueOf(m.group(2));
                final int b = Integer.valueOf(m.group(3));
                final int a = Integer.valueOf(m.group(4));
                return new java.awt.Color(r, g, b, a);
            }
            return null;
        }
 
    @Override
    public String marshal(java.awt.Color v) throws Exception {
        if (v == null) {
            return null;
        }
        return "rgba(" + v.getRed() + "," + v.getGreen() + "," + v.getBlue() + "," + v.getAlpha() + ")";
    }
}

При использовании этого адаптера в XML мы увидим следующее:
rgba(255,0,0,255)
Идем дальше – почему AlignmentAttribute нужно доработать?
У него value – Enum, а это тоже не простой тип, хотя вроде бы должно быть наоборот – номер и все тут. Но JAXB нужно научить работать с ним. Есть два способа – либо написать адаптер, аналогично Color, либо применить аннотации. Я принял второе решение. Объявляем enum как тип в определенном namespace:

@XmlType(namespace = "AlignmentAttribute")
public enum VALUE {
    NONE, LEFT, RIGHT, CENTER
}

Это нужно для того, что если у нас много разных атрибутов, у которых enum называется одинаково – VALUE, как в имеющейся модели, и JAXB не поймет кто чей.
Переопределим у AlignmentAttribute @XmlElement:

@XmlElement(name = "Value")
@Override
protected VALUE getXmlValue() {
    return value;
}
 
@Override
protected void setXmlValue(VALUE value) {
    this.value = value;
}

По атрибутам все. Теперь мы имеем представление, как доработать остальные для JAXB – атрибуты с простыми типами value остаются без изменений, enum-атрибуты – нуждаются в namespace типа, а атрибуты сложных типов нуждаются в адаптерах.
Переходим к элементам.
Аннотируем класс TerminalElement через @XmlAccessorType(XmlAccessType.NONE) – как уже описывалось выше, чтобы исключить ненужный маппинг всех полей и методов по умолчанию. Вот BranchElement, благодаря этому, уже не нуждается в такой аннотации — она есть в супер-классе TerminalElement, и этого достаточно.
Да, еще одним важным условием для XML-cериализации является наличие конструктора без параметров, поэтому BranchElement как раз содержит такой пустой конструктор.
Далее интереснее – добавим в TextElement следующее:

@XmlElement(name = "Text")
protected String getXmlText(){
    return text;
}
 
protected void setXmlText(String text){
    this.text = text;
}

Это значит, что в XML будет элемент с именем «Text», содержащий текст TextElement – все просто:
test!!!
Для эксперимента вместо элемента можно использовать атрибут:

@ XmlAttribute (name = "Text")
protected String getXmlText(){
    return text;
}

И в XML мы увидим:

А что с атрибутами? JAXB может сериализовать автоматически не только простые типы и String, но и простые наборы данных типа массив и List, где T – любой тип поддерживающий XML-сериализацию, т.е. подготовленный для JAXB.
Что делать с нашей LinkedHashMap из TerminalElement? Правильно – писать адаптер, например AttributeListAdapter:

public static class AttributeListAdapter extends AbstractList {
        protected final Map<Class, Attribute> map;
 
        public AttributeListAdapter(Map<Class, Attribute> map){
            this.map = map;
        }
 
        @Override
        public boolean add(Object o) {
            if (o == null){
                return false;
            }
            return this.map.put(o.getClass(), (Attribute)o) != null;
        }
 
        @Override
        public Attribute get(int index) {
            final Iterator iterator = map.values().iterator();
            int n = 0;
            while(iterator.hasNext()){
                final Attribute attribute = (Attribute) iterator.next();
                if(n == index){
                    return attribute;
                }
                n++;
            }
            return null;
        }
 
        @Override
        public Iterator iterator() {
            return this.map.values().iterator();
        }
 
        @Override
        public int size() {
            return this.map.values().size();
        }
    }

Адаптер умеет передавать элементы из Map в виде простого списка элементов в JAXB, и наоборот принимать элементы из простого списка JAXB и записывать их в Map – все просто.
А в TextElement теперь нужно сделать оболочку (wrapper) для списка атрибутов:

@XmlElementWrapper(name = "Attributes")
@XmlElements({
        @XmlElement(name = "Bold", type = BoldAttribute.class),
        @XmlElement(name = "Italic", type = ItalicAttribute.class),
        @XmlElement(name = "Color", type = ColorAttribute.class),
        @XmlElement(name = "FontName", type = FontNameAttribute.class),
        @XmlElement(name = "FontSize", type = FontSizeAttribute.class)
    })
protected List<Attribute> getXmlAttributes() {
    return new AttributeListAdapter(attributes);
}

Т.е. мы указываем, что в XML список будет находиться в элементе с именем «Attributes», а каждый встреченный атрибут так же получает определенное имя: BoldAttribute – Bold, FontNameAttribute – FontName и т.д.
Аналогично описываем атрибуты ParagraphElement:

@XmlElementWrapper(name = "Attributes")
@XmlElements({
        @XmlElement(name = "Indent", type = IndentAttribute.class),
        @XmlElement(name = "Alignment", type = AlignmentAttribute.class)
})
protected List<Attribute> getXmlAttributes() {
    return newAttributeListAdapter(attributes);
}

И последнее – нужна оболочка (wrapper) для списка элементов для элемента-контейнера. Например, для ParagraphElement:

@XmlElementWrapper(name = "Elements")
@XmlElements({
        @XmlElement(name = "TextElement", type = TextElement.class)
})
protected List<TerminalElement> getXmlElements(){
    return elements;
}

В XML список подэлементов будет в элементе с именем «Elements».
Т.к. elements уже простой список List<TerminalElement>, то писать адаптер не нужно – JAXB справится с ним автоматически.
Для RootElement аналогично:

@XmlElementWrapper(name = "Elements")
@XmlElements({
        @XmlElement(name = "ParagraphElement", type = ParagraphElement.class)
})
protected List<TerminalElement> getXmlElements(){
    return elements;
}

Теперь осталось написать методы сохранения/загрузки модели в/из XML-файла, добавим их прямо в тест в main():
Сохранение в XML-файл:

final Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(re, new File("c:\\test_jaxb.xml"));

Загрузка из XML-файла:

final Unmarshaller unmarshaller = jc.createUnmarshaller();
final RootElement re2 = (RootElement) unmarshaller.unmarshal(new File("c:\\ test_jaxb.xml"));

Модель из примера в XML-файле будет выглядеть так:



    
        
            
                
                    1000
                
                
                    CENTER
                
            
            
                
                    
                        
                            true
                        
                        
                            true
                        
                        
                            rgba(255,0,0,255)
                        
                        
                            Arial
                        
                        
                            14
                        
                    
                    test!!!
                
            
        
    

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

Файлы с исходным кодом: src / Применение JAXB для XML-сериализации объектной модели с иерархией наследования.

Опубликовано в Allgemein, актуально, статья