CodeGym /課程 /JAVA 25 SELF /設定 XML 序列化:自訂配接器

設定 XML 序列化:自訂配接器

JAVA 25 SELF
等級 47 , 課堂 4
開放

1. 使用配接器 (@XmlJavaTypeAdapter)

JAXB 很像自排變速箱:一切都很標準時它運作完美,但一遇到不尋常的情況,就需要手動介入。想像你在類別中加入一個 LocalDateBigDecimal 欄位。JAXB 會犯難——它並不知道如何把它們轉成 XML 再轉回來。或者你希望日期不要是冗長的 2024-06-01T00:00:00,而是習慣的 01.06.2024 格式。又或者你有個物件更適合存成屬性而不是元素,或是一個包含巢狀物件的集合,需要特殊呈現。

這些情況都可以透過配接器解決。借助它們,你可以告訴 JAXB 應該如何序列化與反序列化複雜欄位、指定所需格式,甚至略過不需要的資料。這就像手排換檔,在自排處理不了的地方提供彈性。

什麼是配接器?

配接器是一個特殊類別,用來告訴 JAXB:「如果遇到這種型別,序列化要這樣做,反序列化要那樣做」。在 Java 中,配接器會實作抽象類別 javax.xml.bind.annotation.adapters.XmlAdapter<ValueType, BoundType>,其中:

  • ValueType — 資料在 XML 中的表示形式(通常是 String,有時是 IntegerLong,甚至是另一個物件)。
  • 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 的無參數建構子

    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:在欄位或 getter 上標註註解

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }

或者可以把註解加在欄位上:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;

步驟 3:檢查結果

現在序列化後的物件會長這樣:

<Person>
    <name>Ivan</name>
    <birthDate>01.06.2024</birthDate>
</Person>

反向亦然——從 XML 讀取時,字串 "01.06.2024" 會被轉為 LocalDate 物件。

2. 將配接器套用到欄位、getter 或整個類別

配接器可以用不同方式套用。

針對單一欄位或 getter: 這是最常見的情況。

@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>Ivan</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;
    }

    // getters 與 setters...
}

我們得到:

  • name 被序列化為元素 <name>
  • 生日被序列化為屬性 <Person birth_date="01.06.2024">
  • balance 被序列化為元素 <balance>1234.56</balance>
  • password 完全不會出現在 XML 中。

XML 檔案:

<Person birth_date="01.06.2024">
    <name>Ivan</name>
    <balance>1234.56</balance>
</Person>

6. 處理集合與巢狀物件

JAXB 能處理集合,只要標註正確的註解。例如,如果某人有地址清單:

@XmlElementWrapper(name = "addresses")
@XmlElement(name = "address")
private List<Address> addresses;

在 XML 中會是這樣:

<addresses>
    <address city="Berlin">
        <street>Alexanderplatz, 1</street>
    </address>
    <address city="Limassol">
        <street>Anexartisias, 10</street>
    </address>
</addresses>

如果集合中的型別不是標準型別(例如 List<LocalDate>),可以把配接器套用到集合元素:

@XmlElementWrapper(name = "dates")
@XmlElement(name = "date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private List<LocalDate> importantDates;

7. 範例:使用配接器進行序列化與反序列化

序列化

Person person = new Person(
    "Ivan",
    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>Ivan</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:缺少 public 的無參數建構子。 如果沒有,JAXB 無法在反序列化時建立物件,並會丟出例外。

錯誤 2:錯誤套用配接器。 如果把 @XmlJavaTypeAdapter 放錯位置,或遺漏 getter/setter,JAXB 將不知道如何序列化目標型別。

錯誤 3:反序列化時格式不相符。 如果 XML 中的日期格式不受你的配接器支援(例如 "2024-06-01" 而非 "01.06.2024"),unmarshal 方法會丟出例外。

錯誤 4:未使用配接器就嘗試序列化 JAXB 不支援的型別。 典型例子包括 LocalDateBigDecimalMap,以及自訂的複雜型別。

錯誤 5:巢狀集合缺少註解而被忽略。 沒有 @XmlElementWrapper 時,集合的序列化可能與預期不同,或 JAXB 根本無法正確讀回 XML。

錯誤 6:把配接器套用在集合本身而非元素。 若要透過配接器序列化清單中的元素,請把註解放在元素上,而不是放在集合本身(例如把 @XmlJavaTypeAdapter 標在元素欄位上,或像前述範例那樣標在清單欄位上並指明元素型別)。

1
問卷/小測驗
XML 序列化,等級 47,課堂 4
未開放
XML 序列化
XML 序列化
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION