Статьи

JavaBeans в XML без библиотек

Преобразование JavaBeans в XML и наоборот является довольно распространенной задачей, и для этой цели существует множество библиотек. Но мне всегда нравится использовать то, что уже доступно в JRE, максимально избегая зависимостей. В прошлом я и Симона разработали API отдыха с использованием сериализации JSR57 , которая уже была доступна в Java 5. Но Java 6 включила JAXB в стандартные библиотеки, которая является более современной и гибкой, и дает вам лучший контроль над XML процесс перевода.

Здесь я хочу привести пару простых примеров, чтобы показать, как работают эти два API.

Предположим, у нас есть некоторые Java-бины «модель» согласно следующему источнику:

public class Book implements Serializable {
String title;
String author;
Price price;
public Book() {} //default constructor is mandatory for JavaBeans

public Book(String title, String author, Price price) {
this.title= title;
this.author = author;
this.price = price;
}

//... imagine the getter and setters boilerplate code here ...

@Override
public String toString() {
return title + " by " + author + ", " + price;
}

}

public class Price implements Serializable {
Double amount;
Currency currency;

public Price() {} // default constructor, as JavaBeans mandate...

public Price(Double amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}

//... imagine the getter and setters boilerplate code here ...

@Override
public String toString() {
DecimalFormat fmt = new DecimalFormat("0.00");
return fmt.format(amount) + currency;
}
}

Два простых JavaBeans, классический пример: один моделирует книгу с названием, автором и ценой. И цена состоит из суммы и валюты. Класс
Currency со скрытым партнерским
классом
CurrencyData — забавная пара в JDK; если вы посмотрите на код, вы можете найти его довольно забавным; Я предлагаю никогда не использовать это. Кстати, это хороший пример, потому что у этого класса, кроме такого исходного кода, нет общедоступного конструктора по умолчанию, что приводит к сбою маршалинга XML. Так что это хороший пример для не столь тривиальной сериализации XML.

В следующих примерах кода создается экземпляр объекта Book в соответствии с приведенными выше определениями класса, затем преобразуется в строку XML, а после этого строка XML будет использоваться для восстановления другого экземпляра объекта Book.

Сериализатор JSR57. Также известный как «XMLDecoder / XMLEncoder»

Давайте начнем с сериализатора JSR57.

public class Jsr57Spike {
public static void main(String[] args) throws Exception {
Book book = new Book("Carrie", "Stephen King", new Price(17.25,
Currency.getInstance("CHF")));

String xml = encode(book);
System.out.println(xml);

Object o = decode(xml);
System.out.println("decoded object: " + o);
}

private static String encode(Book book) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLEncoder encoder = new XMLEncoder(out);

fixCurrency(encoder);

encoder.writeObject(book);
encoder.close();
return out.toString();
}

private static void fixCurrency(XMLEncoder encoder) {
encoder.setPersistenceDelegate(Currency.class,
new PersistenceDelegate() {
@Override
protected Expression instantiate(Object oldInstance,
Encoder out) {
return new Expression(Currency.class, "getInstance",
new Object[] { oldInstance.toString() });
}
});
}

private static Object decode(String xml)
throws UnsupportedEncodingException {
XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(xml
.getBytes("UTF-8")));
return decoder.readObject();
}
}

Как видите, мне пришлось написать специальный обработчик для работы с классом java.util.Currency (строки 25..33), перед использованием необходимо установить PersistencyDelegate для класса Currency в кодировщике. Если вы этого не сделаете, он не сгенерирует никаких исключений, но декодер не сможет десериализовать XML, поскольку не сможет создать экземпляр класса Currency. Он не выдаст никаких исключений, он просто напечатает на System.error некоторые странные вещи, такие как:

java.lang.InstantiationException: java.util.Currency
Continuing ...
java.lang.RuntimeException: failed to evaluate: <unbound>=Class.new();
Continuing ...

И декодированный объект пропустит экземпляры валюты. Вы можете легко попробовать это, комментируя строку № 17, где применяется «исправление».

После исправления все должно пройти нормально и вывести следующий вывод:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.6.0_20" class="java.beans.XMLDecoder">
<object class="it.newinstance.xml.spike.model.Book">
<void property="author">
<string>Stephen King</string>
</void>
<void property="price">
<object class="it.newinstance.xml.spike.model.Price">
<void property="amount">
<double>17.25</double>
</void>
<void property="currency">
<object class="java.util.Currency" method="getInstance">
<string>CHF</string>
</object>
</void>
</object>
</void>
<void property="title">
<string>Carrie</string>
</void>
</object>
</java>

decoded object: Carrie by Stephen King, 17.25CHF

Мы можем обнаружить, что приведенное выше представление XML, возможно, слишком многословно и слишком ориентировано на Java. Так что мы можем с нетерпением ждать Jaxb …

Но здесь приятно отметить, что в этом случае мне не пришлось трогать модель (класс Book и Price), поскольку XMLDecoder / Encoder действительно не навязчив и может подойти для JavaBeans, из которого у нас нет исходного кода или мы не можем легко изменить

JAXB: архитектура Java для привязки XML

Следующий код снова делает то же самое: от JavaBean до XML и обратно.

public class JaxbSpike {
@XmlRootElement
private static class XMLBook extends Book {
@SuppressWarnings("unused")
public XMLBook() {} // default constructor is mandated by JavaBeans spec

public XMLBook(String title, String author, Price price) {
super(title, author, price);
}
}

public static void main(String[] args) throws JAXBException {
JAXBContext jc = JAXBContext.newInstance(XMLBook.class);
Book book = new XMLBook("Carrie", "Stephen King", new Price(17.25,
Currency.getInstance("EUR")));

String xml = marshall(jc, book);
System.out.println(xml);

Book unmashalledBook = unmarshall(jc, xml);
System.out.println("Last book I read: " + unmashalledBook);
}

private static Book unmarshall(JAXBContext jc, String xml)
throws JAXBException {
Unmarshaller u = jc.createUnmarshaller();
return (Book) u.unmarshal(new StringReader(xml));
}

private static String marshall(JAXBContext jc, Book book)
throws JAXBException, PropertyException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Marshaller m = jc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(book, out);
return out.toString();
}
}

Jaxb требует, чтобы объект, представляющий корень XML, был аннотирован @XmlRootElement. Поскольку я не люблю менять свою модель просто для ее преобразования, в этом примере я предпочел создать подкласс Book и применить сериализацию XML к внутреннему классу XMLBook, где я могу добавить аннотацию без каких-либо проблем.

В строке 34 я установил для свойства JAXB_FORMATTED_OUTPUT значение true, чтобы создать форматированный XML. Это полезно для чтения человеком, но хорошо иметь возможность создавать XML в одной строке, чтобы оптимизировать процесс на машинах.

Остальная часть кода не требует пояснений.

Здесь мне не нужно было добавлять какой-либо обработчик для класса Currency в коде маршаллера / демаршаллера … Но мне пришлось объявить конвертер с пометкой, примененной к пакету. Чтобы аннотировать пакет, содержащий классы модели, вам нужно создать специальный файл с именем «package-info.java» со следующим источником:

@XmlJavaTypeAdapter(value=CurrencyAdapter.class,type=Currency.class)
package it.newinstance.xml.spike.model;
import java.util.Currency;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

Маршаллер / unmarshaller проверит аннотации, примененные к классам, полям и пакетам, и применит указанные правила. JAXB имеет богатый набор аннотаций, которые можно применять к вашим JavaBeans и к самим пакетам. Проблема в том, что аннотации являются, во всех аспектах, элементами интерфейса. Аннотирование класса означает введение зависимостей и изменение интерфейсов модели вашего домена. Также имейте в виду, что иногда вы просто не владеете исходным кодом JavaBeans, который хотите сериализовать, или вы не можете их изменить. Подклассы могут помочь, как я показал в приведенном выше примере, представить внутренний класс XMLBook, но этого может быть недостаточно. Дальнейшее изучение API JAXB может показать решения этой проблемы, но на данный момент я не знаю …

Затем мне также пришлось создать очень простой класс CurrencyAdapter, который будет использоваться JAXB для обработки переводов Java <-> XML

public class CurrencyAdapter extends XmlAdapter<String, Currency>{

@Override
public String marshal(Currency v) throws Exception {
return v.toString();
}

@Override
public Currency unmarshal(String v) throws Exception {
return Currency.getInstance(v);
}
}

Если вы не настроили CurrencyAdapter, вы получите следующее исключение:

Exception in thread "main" com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
  java.util.Currency does not have a no-arg default constructor.
	this problem is related to the following location:
		at java.util.Currency
		at public java.util.Currency it.newinstance.xml.spike.model.Price.getCurrency()
		at it.newinstance.xml.spike.model.Price
		at public it.newinstance.xml.spike.model.Price it.newinstance.xml.spike.model.Book.getPrice()
		at it.newinstance.xml.spike.model.Book
		at it.newinstance.xml.spike.jaxb.JaxbSpike$XMLBook

	at com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException$Builder.check(IllegalAnnotationsException.java:91)
	at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:436)
	at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.<init>(JAXBContextImpl.java:277)
	at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1100)
	at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:143)
	at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:110)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:202)
	at javax.xml.bind.ContextFinder.find(ContextFinder.java:376)
	at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:574)
	at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:522)
	at it.newinstance.xml.spike.jaxb.JaxbSpike.main(JaxbSpike.java:32)

Я должен сказать, что исключение очень описывает проблему. JAXB, для тех немногих, кого я имел возможность увидеть, это очень хорошо сделанный API.

Итак, теперь, когда у нас все есть, давайте посмотрим, что получится у нашего JaxbSpike:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xmlBook>
<author>Stephen King</author>
<price>
<amount>17.25</amount>
<currency>EUR</currency>
</price>
<title>Carrie</title>
</xmlBook>

Last book I read: Carrie by Stephen King, 17.25EUR

Конечно, лучшее, более простое и не Java-ориентированное представление объекта Book, если мы сравним это с выводом, полученным с помощью XMLEncoder.

Но мы должны заметить, что мы добавили аннотацию на пакет, поэтому мы изменили исходный домен. Вполне возможно, что, изучая API JAXB лучше, этого изменения можно избежать, но я действительно не уверен в этом. Наверняка JAXB — лучший выбор, когда у вас есть некоторая гибкость в изменении кода вашего JavaBeans.

Хотите попробовать сами?

Здесь вы найдете исходный код: JavaBeansToXml.tar.gz

Удачи!

PS Это все, что я знаю о сериализации JAXB и JSR57 XML; если у вас есть конкретная проблема, лучше обратиться на какой-нибудь форум сообщества. Но не стесняйтесь оставлять комментарии, я буду рад помочь, если узнаю ответ.

С http://en.newinstance.it/2010/08/05/javabeans-to-xml-with-no-libraries/