1. Використання адаптерів (@XmlJavaTypeAdapter)
JAXB справді схожий на автоматичну коробку передач: поки все стандартно — він працює ідеально, але варто зустріти щось незвичне, і без ручного втручання вже не обійтися. Уявіть: ви додали у свій клас поле типу LocalDate або BigDecimal. JAXB розгубиться — він просто не знає, як перетворити їх у XML і назад. Або ви хочете, щоб дата виглядала не як довгий рядок 2024-06-01T00:00:00, а у звичному форматі 01.06.2024. А може, у вас є об’єкт, який логічніше зберігати в атрибуті, а не в елементі, або колекція з вкладеними об’єктами, що потребує особливого подання.
Усі ці ситуації вирішуються через адаптери. З їхньою допомогою можна підказати JAXB, як саме серіалізувати та десеріалізувати складні поля, задати потрібний формат або навіть пропустити зайві дані. Це і є ручне керування «передачами», яке дає гнучкість там, де автомат уже не справляється.
Що таке адаптер?
Адаптер — це спеціальний клас, який каже JAXB: «Якщо зустрінеш ось такий тип, серіалізуй його ось так, а десеріалізуй ось так». У Java адаптер реалізує абстрактний клас javax.xml.bind.annotation.adapters.XmlAdapter<ValueType, BoundType>, де:
- ValueType — як дані будуть представлені в XML (зазвичай це String, іноді Integer, Long або навіть інший об’єкт).
- BoundType — реальний тип у вашому Java-класі (наприклад, LocalDate).
Приклад: серіалізація поля типу LocalDate
import java.time.LocalDate;
import javax.xml.bind.annotation.*;
@XmlRootElement
public class Person {
private String name;
private LocalDate birthDate; // Ось тут проблема!
public Person() {} // JAXB вимагає публічний конструктор без параметрів
public Person(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
@XmlElement
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@XmlElement
public LocalDate getBirthDate() { return birthDate; }
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
}
Якщо спробувати серіалізувати такий об’єкт, JAXB викине виняток:
javax.xml.bind.JAXBException: class java.time.LocalDate nor any of its super class is known to this context.
Крок 1: Створюємо адаптер
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
// Адаптер для перетворення LocalDate <-> String
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
@Override
public LocalDate unmarshal(String v) throws Exception {
return (v == null || v.isEmpty()) ? null : LocalDate.parse(v, FORMATTER);
}
@Override
public String marshal(LocalDate v) throws Exception {
return (v == null) ? null : v.format(FORMATTER);
}
}
- marshal — перетворює об’єкт Java (LocalDate) на рядок для XML.
- unmarshal — перетворює рядок із XML назад на об’єкт Java.
Крок 2: Позначаємо поле або геттер анотацією
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }
Або можна поставити анотацію на саме поле:
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;
Крок 3: Перевіряємо результат
Тепер під час серіалізації об’єкт виглядатиме так:
<Person>
<name>Іван</name>
<birthDate>01.06.2024</birthDate>
</Person>
І навпаки — під час читання з XML рядок "01.06.2024" перетвориться на об’єкт LocalDate.
2. Застосування адаптера до поля, геттера або всього класу
Адаптер можна застосовувати по-різному.
До окремого поля або геттера: Це найчастіший випадок.
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;
До цілого класу: Якщо ви хочете, щоб JAXB завжди серіалізував певний тип через адаптер, можна позначити сам клас:
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public class LocalDate { ... }
Зазвичай так роблять для власних класів, а не для стандартних (клас LocalDate модифікувати не можна).
До колекцій: Можна серіалізувати, наприклад, List<LocalDate> через адаптер, який перетворює список дат на список рядків.
3. Налаштування назв елементів і атрибутів
Іноді вимоги до XML-структури жорсткі: наприклад, замовник хоче, щоб поле називалося не <birthDate>, а <birth_date>, або щоб дата народження була атрибутом, а не елементом.
Зміна назви елемента
@XmlElement(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }
У XML тепер буде:
<birth_date>01.06.2024</birth_date>
Серіалізація як атрибут
@XmlAttribute(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }
У XML:
<Person birth_date="01.06.2024">
<name>Іван</name>
</Person>
Поєднання з адаптером
@XmlAttribute(name = "birth_date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }
4. Практичні кейси
Пропуск полів (@XmlTransient)
Іноді потрібно, щоб якесь поле не потрапляло в XML взагалі (наприклад, внутрішній ідентифікатор, пароль, тимчасові дані).
@XmlTransient
private String internalCode;
Таке поле буде проігнороване під час серіалізації та десеріалізації.
Форматування чисел
Припустімо, у вас є поле з грошовою сумою:
private BigDecimal balance;
JAXB не вміє серіалізувати BigDecimal у потрібному вам форматі (наприклад, із двома знаками після десяткового роздільника). Пишемо адаптер:
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.math.BigDecimal;
public class BigDecimalAdapter extends XmlAdapter<String, BigDecimal> {
@Override
public BigDecimal unmarshal(String v) throws Exception {
return (v == null || v.isEmpty()) ? null : new BigDecimal(v.replace(",", "."));
}
@Override
public String marshal(BigDecimal v) throws Exception {
return (v == null) ? null : String.format("%.2f", v);
}
}
І використовуємо:
@XmlJavaTypeAdapter(BigDecimalAdapter.class)
private BigDecimal balance;
Вкладені структури
Якщо у вас є вкладені об’єкти, наприклад:
public class Address {
private String city;
private String street;
// ...
}
JAXB сам серіалізує вкладені об’єкти як елементи. Але якщо вам потрібно, щоб, наприклад, city був атрибутом, а street — елементом, використовуйте анотації:
public class Address {
@XmlAttribute
private String city;
@XmlElement
private String street;
}
5. Приклад: Повне налаштування серіалізації з адаптером
Розвиваємо застосунок: тепер у нас є клас Person із датою народження та балансом.
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.math.BigDecimal;
import java.time.LocalDate;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
@XmlElement
private String name;
@XmlAttribute(name = "birth_date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;
@XmlElement
@XmlJavaTypeAdapter(BigDecimalAdapter.class)
private BigDecimal balance;
@XmlTransient
private String password;
public Person() {}
public Person(String name, LocalDate birthDate, BigDecimal balance, String password) {
this.name = name;
this.birthDate = birthDate;
this.balance = balance;
this.password = password;
}
// гетери та сетери…
}
Що ми отримали:
- Ім’я серіалізується як елемент <name>.
- Дата народження серіалізується як атрибут <Person birth_date="01.06.2024">.
- Баланс серіалізується як елемент <balance>1234.56</balance>.
- Пароль не потрапляє в XML взагалі.
XML-файл:
<Person birth_date="01.06.2024">
<name>Іван</name>
<balance>1234.56</balance>
</Person>
6. Обробка колекцій і вкладених об’єктів
JAXB уміє працювати з колекціями, якщо вони правильно анотовані. Наприклад, якщо у людини є список адрес:
@XmlElementWrapper(name = "addresses")
@XmlElement(name = "address")
private List<Address> addresses;
У XML це виглядатиме так:
<addresses>
<address city="Берлін">
<street>Александерплац, 1</street>
</address>
<address city="Лімасол">
<street>Анексартісіас, 10</street>
</address>
</addresses>
Якщо тип у колекції нестандартний (наприклад, List<LocalDate>), можна застосувати адаптер до елемента колекції:
@XmlElementWrapper(name = "dates")
@XmlElement(name = "date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private List<LocalDate> importantDates;
7. Приклад: серіалізація та десеріалізація з адаптером
Серіалізація
Person person = new Person(
"Іван",
LocalDate.of(1990, 6, 1),
new BigDecimal("1234.56"),
"secretPassword"
);
JAXBContext context = JAXBContext.newInstance(Person.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(person, System.out); // Виведе XML у консоль
Десеріалізація
String xml = """
<Person birth_date="01.06.1990">
<name>Іван</name>
<balance>1234.56</balance>
</Person>
""";
JAXBContext context = JAXBContext.newInstance(Person.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
Person person = (Person) unmarshaller.unmarshal(new StringReader(xml));
System.out.println(person.getName() + " " + person.getBirthDate() + " " + person.getBalance());
8. Типові помилки під час налаштування серіалізації й адаптерів
Помилка № 1: Відсутність публічного конструктора без параметрів. Якщо його немає, JAXB не зможе створити об’єкт під час десеріалізації та викине виняток.
Помилка № 2: Некоректне застосування адаптера. Якщо поставити @XmlJavaTypeAdapter не на те поле або забути геттер/сеттер, JAXB не знатиме, як серіалізувати потрібний тип.
Помилка № 3: Невідповідність формату під час десеріалізації. Якщо в XML дата записана у форматі, який не підтримує ваш адаптер (наприклад, "2024-06-01" замість "01.06.2024"), метод unmarshal викине виняток.
Помилка № 4: Спроба серіалізувати тип, який JAXB не підтримує, без адаптера. Типовий приклад — LocalDate, BigDecimal, Map, користувацькі складні типи.
Помилка № 5: Ігнорування вкладених колекцій без анотацій. Без @XmlElementWrapper колекція серіалізується не так, як очікується, або JAXB узагалі не зможе коректно прочитати XML назад.
Помилка № 6: Застосування адаптера до колекції, а не до елемента. Якщо ви хочете серіалізувати елементи списку через адаптер, ставте анотацію до елемента, а не до самої колекції (наприклад, @XmlJavaTypeAdapter над полем елемента, або над полем списку з указанням типу елемента, як у прикладах вище).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ