1. Wprowadzenie do JAXB
JAXB (Java Architecture for XML Binding) — to standardowa technologia Javy do przekształcania (binding) obiektów Java do XML i z powrotem. Z pomocą JAXB można łatwo serializować obiekty do plików XML, a następnie odtwarzać je z tych plików.
JAXB wchodził do standardowej biblioteki Javy do wersji 11 włącznie. Począwszy od Java 11, JAXB został wydzielony do osobnego modułu, który trzeba dołączyć przez Maven/Gradle lub pobrać ręcznie. Dla nowoczesnych wersji Javy dodaj zależności:
<!-- Przykład dla Maven -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.3</version>
</dependency>
Po co w ogóle XML?
- XML to uniwersalny, czytelny dla człowieka format, który jest szeroko używany do wymiany danych między systemami, konfigurowania i przechowywania informacji.
- W przeciwieństwie do serializacji binarnej, XML można łatwo przeczytać „gołym okiem”, zweryfikować zgodność ze schematem i otworzyć w przeglądarce.
2. Podstawowe klasy i adnotacje JAXB
JAXB działa na podstawie adnotacji, którymi oznacza się klasy i ich pola, aby sterować procesem serializacji/deserializacji.
Najważniejsze adnotacje
| Adnotacja | Do czego służy |
|---|---|
|
Oznacza korzeniowy element XML (samą klasę) |
|
Oznacza pole/właściwość jako element XML |
|
Oznacza pole/właściwość jako atrybut XML |
|
Steruje kolejnością elementów, nazwą typu i innymi |
|
Wyklucza pole z serializacji |
Najważniejsze klasy
- JAXBContext — punkt wejścia, tworzy kontekst dla serializacji/deserializacji konkretnych klas.
- Marshaller — zamienia obiekt na XML (marshalling, marshal()).
- Unmarshaller — zamienia XML na obiekt (unmarshalling, unmarshal()).
3. Przykład: serializacja obiektu do XML
Utwórzmy klasę, którą będziemy serializować. Niech to będzie postać do naszej gry:
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlAttribute;
@XmlRootElement(name = "player")
public class Player {
private String name;
private int level;
private int health;
public Player() {} // Obowiązkowy pusty konstruktor!
public Player(String name, int level, int health) {
this.name = name;
this.level = level;
this.health = health;
}
@XmlElement
public String getName() {
return name;
}
public void setName(String name) { this.name = name; }
@XmlElement
public int getLevel() {
return level;
}
public void setLevel(int level) { this.level = level; }
@XmlAttribute
public int getHealth() {
return health;
}
public void setHealth(int health) { this.health = health; }
}
- @XmlRootElement(name = "player") — klasa staje się elementem korzeniowym <player>.
- @XmlElement — pole będzie osobnym elementem XML (<name>, <level>).
- @XmlAttribute — pole będzie atrybutem elementu korzeniowego (health="100").
- Nie zapomnij o pustym konstruktorze! JAXB wymaga go do deserializacji.
Serializacja obiektu do XML
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Marshaller;
public class Main {
public static void main(String[] args) throws Exception {
Player player = new Player("Aragorn", 5, 100);
JAXBContext context = JAXBContext.newInstance(Player.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Ładny format wyjścia
marshaller.marshal(player, System.out); // Wypisujemy XML na konsolę
// marshaller.marshal(player, new File("player.xml")); // Albo do pliku
}
}
Wynik:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<player health="100">
<name>Aragorn</name>
<level>5</level>
</player>
Deserializacja obiektu z XML
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.Unmarshaller;
import java.io.File;
public class Main {
public static void main(String[] args) throws Exception {
JAXBContext context = JAXBContext.newInstance(Player.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
Player player = (Player) unmarshaller.unmarshal(new File("player.xml"));
System.out.println(player.getName() + ", poziom: " + player.getLevel() + ", zdrowie: " + player.getHealth());
}
}
4. Cechy i ograniczenia JAXB
Wymagania dla klas
- Publiczny konstruktor bez parametrów — obowiązkowy.
- Dla poprawnego działania używaj getterów i setterów.
- Wszystkie pola podlegające serializacji muszą być dostępne (przez publiczne API).
- Obiekty zagnieżdżone i kolekcje także muszą być serializowalne (oznacz je adnotacjami i dodaj pusty konstruktor).
Praca z kolekcjami i obiektami zagnieżdżonymi
Załóżmy, że gracz ma ekwipunek (listę przedmiotów). Jak zserializować kolekcję?
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import java.util.List;
@XmlRootElement(name = "player")
public class Player {
// ... pozostałe pola
private List<String> inventory;
public Player() {}
// ... pozostałe gettery/settery
@XmlElementWrapper(name = "inventory")
@XmlElement(name = "item")
public List<String> getInventory() {
return inventory;
}
public void setInventory(List<String> inventory) {
this.inventory = inventory;
}
}
Wynik serializacji:
<player health="100">
<name>Aragorn</name>
<level>5</level>
<inventory>
<item>Sword</item>
<item>Shield</item>
</inventory>
</player>
- @XmlElementWrapper — tworzy „opakowanie” wokół kolekcji (element <inventory>).
- @XmlElement(name = "item") — każdy element listy serializowany jest jako <item>.
Jeśli masz obiekty zagnieżdżone (np. Position), również trzeba je oznaczyć adnotacjami i dodać pusty konstruktor.
5. Praktyka: serializacja i deserializacja obiektu do XML
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlAttribute;
import java.util.List;
@XmlRootElement(name = "player")
public class Player {
private String name;
private int level;
private int health;
private List<String> inventory;
private Position position;
public Player() {}
public Player(String name, int level, int health, List<String> inventory, Position position) {
this.name = name;
this.level = level;
this.health = health;
this.inventory = inventory;
this.position = position;
}
@XmlElement
public String getName() { return name; }
@XmlElement
public int getLevel() { return level; }
@XmlAttribute
public int getHealth() { return health; }
@XmlElementWrapper(name = "inventory")
@XmlElement(name = "item")
public List<String> getInventory() { return inventory; }
@XmlElement
public Position getPosition() { return position; }
// settery pominięto dla zwięzłości
}
@XmlRootElement(name = "position")
class Position {
private int x;
private int y;
public Position() {}
public Position(int x, int y) { this.x = x; this.y = y; }
@XmlAttribute
public int getX() { return x; }
@XmlAttribute
public int getY() { return y; }
// settery pominięto
}
Serializacja:
Player player = new Player(
"Aragorn",
5,
100,
List.of("Sword", "Shield", "Potion"),
new Position(10, 20)
);
JAXBContext context = JAXBContext.newInstance(Player.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(player, System.out);
Wynik XML:
<player health="100">
<name>Aragorn</name>
<level>5</level>
<inventory>
<item>Sword</item>
<item>Shield</item>
<item>Potion</item>
</inventory>
<position x="10" y="20"/>
</player>
Deserializacja działa analogicznie: JAXB sam odtworzy obiekty zagnieżdżone i kolekcje, jeśli klasy są poprawnie opisane.
6. Tabela: podstawowe adnotacje JAXB i ich efekt
| Adnotacja | Gdzie używać | Co robi w XML |
|---|---|---|
|
Klasa | Element korzeniowy |
|
Getter/pole | Element wewnątrz XML |
|
Getter/pole | Atrybut elementu |
|
Getter kolekcji | „Opakowanie” kolekcji (np. <list>) |
|
Pole/getter | Wyklucza pole z serializacji |
|
Klasa | Steruje kolejnością elementów, nazwą typu |
7. Cechy i ograniczenia JAXB
Kolejność elementów
Domyślnie JAXB może wypisywać elementy w porządku alfabetycznym. Aby jawnie ustawić kolejność, użyj @XmlType i właściwości propOrder:
@XmlType(propOrder = {"name", "level", "inventory", "position"})
Wykluczanie pól
Aby nie serializować pola/gettera, użyj @XmlTransient:
@XmlTransient
public String getSecretCode() { ... }
Problemy z kolekcjami
- Nie używaj „surowych” kolekcji bez generyków: pisz List<Type>, a nie List.
- Jeśli kolekcja przechowuje obiekty, ich klasy również muszą być oznaczone adnotacjami i mieć pusty konstruktor.
Błędy
- Brak pustego konstruktora — otrzymasz JAXBException podczas unmarshallingu.
- Nieoznaczona adnotacjami klasa zagnieżdżona — JAXB nie zdoła jej zserializować/deserializować.
- Niestandardowe typy (np. LocalDate) wymagają adaptera (@XmlJavaTypeAdapter).
8. Typowe błędy podczas pracy z JAXB
Błąd nr 1: Brak pustego konstruktora. JAXB wymaga, aby serializowana klasa miała publiczny konstruktor bez parametrów. Jeśli go nie ma — przy unmarshallingu wystąpi wyjątek JAXBException.
Błąd nr 2: Nieoznaczone adnotacjami obiekty zagnieżdżone. Jeśli masz pole-obiekt, ale jego klasa nie jest oznaczona @XmlRootElement lub przynajmniej @XmlType, JAXB nie będzie w stanie poprawnie go zserializować/deserializować.
Błąd nr 3: Problemy z kolekcjami. JAXB nie rozumie „surowych” kolekcji bez wskazania typu elementów. Używaj generyków i poprawnie oznaczaj kolekcje (@XmlElementWrapper + @XmlElement).
Błąd nr 4: Niejawne sterowanie kolejnością elementów. Jeśli kolejność elementów w XML jest ważna dla integracji, użyj @XmlType z propOrder; w przeciwnym razie JAXB może wypisać elementy w innej kolejności (np. alfabetycznie).
Błąd nr 5: Użycie niestandardowych typów bez adaptera. JAXB nie potrafi serializować niektórych typów (np. LocalDate) bez adaptera. Zastosuj @XmlJavaTypeAdapter lub serializuj wartość jako łańcuch znaków.
GO TO FULL VERSION