В мире 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-сериализации объектной модели с иерархией наследования.