JavaRush /Курси /JAVA 25 SELF /Налаштування серіалізації XML: власні адаптери

Налаштування серіалізації XML: власні адаптери

JAVA 25 SELF
Рівень 47 , Лекція 4
Відкрита

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 над полем елемента, або над полем списку з указанням типу елемента, як у прикладах вище).

1
Опитування
Серіалізація XML, рівень 47, лекція 4
Недоступний
Серіалізація XML
Серіалізація XML
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ