Serializacja XML Java
W tym podrozdziale obiekty będziemy modyfikować za pomocą JAXB (ang. Java Architecture for XML Binding). Jak się przekonasz, JAXB umożliwia serializację XML — określaną przez JAXB terminem marshaling. Obiekt po serializacji to dane obiektu przedstawione w formacie XML. Po zapisie obiektu do pliku można go w dowolnym terminie odczytać i zdeserializować — dane zawarte w XML posłużą do odtworzenia oryginalnego obiektu w pamięci.
Tworzenie sekwencyjnego pliku używającego serializacji XML
Serializacja, którą przedstawimy w tym punkcie, bazuje na strumieniach tekstowych, więc wynik znajdzie się w pliku, który można otworzyć dowolnym edytorem tekstu. Zacznijmy od utworzenia obiektów i zapisania ich w pliku.
Deklaracja klasy Account
Zaczniemy od zdefiniowana klasy Account (rysunek 15.9), która zawiera informacje o kliencie wykorzystywane w przykładzie. Klasa Account zawiera prywatne zmienne instancjiaccount, firstName, lastName i balance (wiersze od 4. do 7.) oraz metody dostępowe
dla nich. Choć metody ustawiające w tym przykładzie nie walidują danych, w aplikacji produkcyjnej powinny.
2 // Klasa Account do przechowywania rekordów jako obiektów
3 public class Account {
4 private int accountNumber;
5 private String firstName;
6 private String lastName;
7 private double balance;
8
9 // Inicjalizuj obiekt wartościami domyślnymi
10 public Account() {this(0, "", "", 0.0);}
11
12 // Inicjalizuj obiekt przekazanymi wartościami
13 public Account(int accountNumber, String firstName,
14 String lastName, double balance) {
15 this.accountNumber = accountNumber;
16 this.firstName = firstName;
17 this.lastName = lastName;
18 this.balance = balance;
19 }
20
21 // Pobierz numer konta
22 public int getAccountNumber() {return accountNumber;}
23
24 // Ustaw numer konta
25 public void setAccountNumber(int accountNumber)
26 {this.accountNumber = accountNumber;}
27
28 // Pobierz imię
29 public String getFirstName() {return firstName;}
30
31 // Ustaw imię
32 public void setFirstName(String firstName)
33 {this.firstName = firstName;}
34
35 // Pobierz nazwisko
36 public String getLastName() {return lastName;}
37
38 // Ustaw nazwisko
39 public void setLastName(String lastName) {this.lastName = lastName;}
40
41 // Pobierz saldo
42 public double getBalance() {return balance;}
43
44 // Ustaw saldo
45 public void setBalance(double balance) {this.balance = balance;}
46 }
Rysunek 15.9. Klasa Account do przechowywania rekordów jako obiektów
Zwykłe obiekty Javy
JAXB korzysta ze standardowych obiektów Javy, czyli tak zwanych POJO (ang. plain old Java objects) — nie są potrzebne żadne klasy nadrzędne i interfejsy, aby obsłużyć serializację XML. Domyślnie JAXB umieszcza w pliku tylko publiczne zmienne instancji, i to takie z publicznymi właściwościami do odczytu i zapisu. Przypomnijmy za punktem 13.5.1, że właściwość do odczytu i zapisu to
właściwość z metodami dostępowymi stosującymi określoną konwencję nazewniczą. W klasie Account metody getAccountNumber i setAccountNumber (wiersze od 22. do 26.) definiują właściwość do odczytu i zapisu o nazwie accountNumber. Podobnie działają metody dostępowe z wierszy od 29. do 45., definiując właściwości firstName, lastName i balance. Klasa musi również zapewniać publiczny konstruktor bezargumentowy lub domyślny, aby możliwe było odtworzenie obiektów w momencie odczytu pliku.
Deklaracja klasy Accounts
Jak zauważysz na rysunku 15.11, przykład przechowuje obiekty Account wewnątrz obiektu typu List, a następnie serializuje całą listę w ramach jednej operacji. Aby móc serializować listę, musimy ją umieścić jako zmienną instancji klasy. Z tego powodu zawarliśmy List wewnątrz klasy Accounts (rysunek 15.10).
2 // Klasa Accounts pozwalająca zapisać całą listę obiektów 3 import java.util.ArrayList;
4 import java.util.List;
5 import javax.xml.bind.annotation.XmlElement;
6
7 public class Accounts {
8 // Adnotacja @XmlElement określa element XML dla każdego obiektu z listy List
9 @XmlElement(name="account")
10 private List accounts = new ArrayList<>(); // Przechowuje Accounts
11
12 // Zwraca List
13 public List getAccounts() {return accounts;}
14 }
Rysunek 15.10. Klasa Accounts pozwalająca zapisać całą listę obiektów
Wiersze 9. i 10. deklarują i inicjalizują zmienną instancji accounts typu List . JAXB umożliwia dostosowywanie wielu aspektów serializacji XML, w tym również serializację prywatnych zmiennych instancji lub właściwości tylko do odczytu. Adnotacja @XMLElement (wiersz 9.; pakiet javax.xml.bind.annotation) wskazuje, że tę zmienną prywatną należy zserializować. Argument name adnotacji omówimy za chwilę. Adnotacja jest niezbędna, bo zmienna instancji nie jest publiczna i nie stosuje publicznej właściwości do odczytu i zapisu.
Zapis obiektów w postaci XML do pliku
Program z rysunku 15.11 zapisuje obiekt Accounts do pliku tekstowego. Program ten podobny jest do programu z podrozdziału 15.4, więc skupimy się tylko na opisie nowych elementów. Wiersz 9. importuje klasę JAXB pakietu javax.xml.bind. Pakiet zawiera wiele powiązanych klas implementujących serializację XML, ale klasa JAXB zapewnia łatwe w użyciu metody statyczne z większością popularnych operacji.
2 // Zapisywanie obiektów w pliku za pomocą JAXB i BufferedWriter
3 import java.io.BufferedWriter;
4 import java.io.IOException;
5 import java.nio.file.Files;
6 import java.nio.file.Paths;
7 import java.util.NoSuchElementException;
8 import java.util.Scanner;
9 import javax.xml.bind.JAXB;
10
11 public class CreateSequentialFile {
12 public static void main(String[] args) {
13 // Otwiera clients.xml, zapisuje obiekty i zamyka plik
14 try(BufferedWriter output =
15 Files.newBufferedWriter(Paths.get("clients.xml"))) {
16
17 Scanner input = new Scanner(System.in);
18
19 // Przechowuje Accounts przed serializacją XML
20 Accounts accounts = new Accounts();
21
22 System.out.printf("%s%n%s%n? ",
23 "Wpisz numer konta, imię, nazwisko i saldo.",
24 "Wpisz wskaźnik końca danych, aby zakończyć.");
25
26 while (input.hasNext()) { // Bądź w pętli aż do wskaźnika końca danych
27 try {
28 // Utwórz nowy rekord
29 Account record = new Account(input.nextInt(),
30 input.next(), input.next(), input.nextDouble());
31
32 // Dodaj do AccountList
33 accounts.getAccounts().add(record);
34 }
35 catch (NoSuchElementException elementException) {
36 System.err.println("Niepoprawne dane. Spróbuj ponownie.");
37 input.nextLine(); // Pomiń dane, aby użytkownik mógł spróbować ponownie
38 }
39
40 System.out.print("? ");
41 }
42
43 // Zapisz obiekt AccountList jako XML
44 JAXB.marshal(accounts, output);
45 }
46 catch (IOException ioException) {
47 System.err.println("Błąd otwarcia pliku. Kończę działanie.");
48 }
49 }
50 }
Wpisz numer konta, imię, nazwisko i saldo.
Wpisz wskaźnik końca danych, aby zakończyć.
? 100 Jan Kowalski 24,98
? 200 Anna Nowak -345,67
? 300 Zofia Czekaj 0,00
? 400 Ola Rudnik -42,16
? 500 Jakub Sroka 224,62
? ^Z
Rysunek 15.11. Zapisywanie obiektów w pliku za pomocą JAXB i BufferedWriter
Aby otworzyć plik, wiersze 14. i 15. wywołują metodę statyczną newBufferedWriter klasy Files, która przyjmuje obiekt Path ze ścieżką do pliku ("clients.xml"), który ma być otwarty do zapisu. Zwraca BufferedWriter, którego klasa JAXB użyje do zapisu tekstu do pliku. Jeśli plik istnieje, zostanie on przycięty i nowa treść nadpisze istniejącą. Standardowym rozszerzeniem dla plików XML
jest .xml. Wiersze 14. i 15. zgłaszają wyjątek IOException, jeśli pojawi się błąd w trakcie otwierania pliku, bo na przykład program nie ma odpowiednich uprawnień lub plik istnieje, ale znajduje się w trybie tylko do odczytu. Program w takiej sytuacji wyświetla komunikat o błędzie (wiersze od 46. do 48.) i kończy działanie. W przeciwnym razie zmienna output służy do zapisywania danych do pliku.
Wiersz 20. tworzy obiekt Accounts zawierający List. Wiersze od 26. do 41. wyświetlają każdy rekord, tworzą obiekt Account (wiersz 29. i 30.) i dodają go do listy (wiersz 33.).
Gdy użytkownik wprowadzi znacznik końca pliku, aby zakończyć wpisywanie danych, wiersz 44. używa metody statycznej marshal z JAXB, aby zserializować do formatu XML obiekt Accounts zawierający obiekt typu List. Drugim argumentem tej przeciążonej wersji metody marshal jest obiekt typu Writer (pakiet java.io), który służy do zapisania danych XML — BuffredWriter to podklasa Writer.
Zwróć uwagę, że wystarczy tylko jedna instrukcja, aby zapisać cały obiekt Accounts i znajdujące się w nim elementy, czyli List. Jako przykładowe wartości w programie z rysunku 15.11 wpisaliśmy pięć kont — dokładnie tych samych danych użyliśmy wcześniej na rysunku 15.5.
Wynikowe dane w formacie XML
Rysunek 15.12 przedstawia zawartość pliku clients.xml. Choć nie musisz w tym przykładzie znać szczegółów działania XML, z pewnością zauważysz, że jest to format czytelny dla ludzi. Gdy JAXB serializuje obiekt klasy, używa nazwy klasy z małą pierwszą literą jako nazwy elementu XML, więc element accounts (wiersze od 2. do 33.) reprezentuje obiekt Accounts.
Rysunek 15.12. Zawartość pliku clients.xml
Przypomnijmy, że wiersz 9. klasy Accounts (rysunek 15.10) poprzedza instancję zmiennej typu List adnotacją:
@XmlElement(name="account")
Poza poinformowaniem JAXB o zamiarze serializacji zmiennej instancji adnotacja określa nazwę elementu XML ("account"), który posłuży do serializacji obiektów Account w wynikowym kodzie XML. Wiersze od 3. do 8. z rysunku 15.12 reprezentują obiekt Account dla klienta Jan Kowalski. Gdybyśmy nie użyli argumentu name w adnotacji, element XML stosowały w tym miejscu nazwę accounts. Można dostosowywać wiele aspektów serializacji XML. Więcej szczegółów znajdziesz pod adresem:
https://docs.oracle.com/javase/tutorial/jaxb/intro/
Każda właściwość klasy Account ma w kodzie XML element o takiej samej nazwie jak właściwość. Wiersze od 4. do 7. zawierają elementy XML klienta Jan Kowalski — accountNumber, balance, firstName i lastName, które JAXB umieścił w kolejności alfabetycznej, ale nie jest to wymagane lub gwarantowane. Elementy zawierają wartości odpowiadające wartościom właściwości: 100 dla accountNumber, 24.98 dla balance, Jan dla firstName i Kowalski dla lastName. Wiersze od 9. do 32. zawierają pozostałe cztery obiekty Account wpisane w przykładowej aplikacji.
Odczyt i deserializacja danych z pliku sekwencyjnego
W poprzednim punkcie przedstawiliśmy tworzenie pliku XML zawierającego dane obiektów. W tym punkcie przyjrzymy się odczytowi zserializowanych danych z pliku. Rysunek 15.13 przedstawia odczyt obiektów z pliku utworzonego przez program z punktu 15.5.1 i wyświetla ich zawartość. Program otwiera plik w celu odczytu, wywołując metodę statyczną newBufferedReader klasy Files, która otrzymuje obiekt Path ze ścieżką pliku. Jeśli plik istnieje i nie pojawi się żaden wyjątek, metoda zwraca obiekt BufferedReader.
1 // Rysunek 15.13. ReadSequentialFile.java
2 // Odczyt obiektów serializowanych do XML za pomocą JAXB
3 // i BufferedReader i ich wyświetlenie
4 import java.io.BufferedReader;
5 import java.io.IOException;
6 import java.nio.file.Files;
7 import java.nio.file.Paths;
8 import javax.xml.bind.JAXB;
9
10 public class ReadSequentialFile {
11 public static void main(String[] args) {
12 // Spróbuj otworzyć plik w celu deserializacji
13 try(BufferedReader input =
14 Files.newBufferedReader(Paths.get("clients.xml"))) {
15 // Zdekoduj zawartość pliku
16 Accounts accounts = JAXB.unmarshal(input, Accounts.class);
17
18 // Wyświetl zawartość
19 System.out.printf("%-10s%-12s%-12s%10s%n", "Konto",
20 "Imię", "Nazwisko", "Saldo");
21
22 for (Account account : accounts.getAccounts()) {
23 System.out.printf("%-10d%-12s%-12s%10.2f%n",
24 account.getAccountNumber(), account.getFirstName(),
25 account.getLastName(), account.getBalance());
26 }
27 }
28 catch (IOException ioException) {
29 System.err.println("Błąd otwarcia pliku.");
30 }
31 }
32 }
Konto Imię Nazwisko Saldo
100 Jan Kowalski 24,98
200 Anna Nowak -345,67
300 Zofia Czekaj 0,00
400 Ola Rudnik -42,16
500 Jakub Sroka 224,62
Rysunek 15.13. Odczyt obiektów serializowanych do XML za pomocą JAXB i BufferedReader i ich wyświetlenie
Wiersz 16. używa metody statycznej unmarshal klasy JAXB, aby odczytać zawartość pliku clients.xml i zamienić dane XML na obiekt Accounts. Przeciążona wersja metody unmarshal odczytuje dane XML z obiektu Reader (pakiet java.io) i tworzy obiekty typu wskazanego jako drugi argument. Klasa BufferedReader jest podklasą klasy Reader. Obiekt BufferedReader uzyskany w wierszach 13. i 14. odczytuje tekst z pliku. Drugi argument metody to tak naprawdę obiekt typu Class (pakiet java.lang) reprezentujący obiekty do utworzenia z XML — zapis Accounts.class to skrót dla:
Zwróć uwagę, że jedna instrukcja odczytuje cały plik i odtwarza obiekt Accounts. Jeśli nie pojawi się żaden wyjątek, wiersze od 19. do 26. wyświetlą zawartość obiektu Accounts.
