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
Задача
JAVA 25 SELF, 47 уровень, 4 лекция
Недоступна
Запись Даты События как Атрибута 📅
Запись Даты События как Атрибута 📅
1
Задача
JAVA 25 SELF, 47 уровень, 4 лекция
Недоступна
Управление Списком Праздничных Дат в Календаре 🎉
Управление Списком Праздничных Дат в Календаре 🎉
1
Опрос
Сериализация XML, 47 уровень, 4 лекция
Недоступен
Сериализация XML
Сериализация XML
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
kasnil Уровень 38
12 февраля 2026
После JEP 320: Remove the Java EE and CORBA Modules, вместо удаленного javax.xml используется отдельный пакет Jakarta XML Binding API с JDK (11+).