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 的無參數建構子
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 不支援的型別。 典型例子包括 LocalDate、BigDecimal、Map,以及自訂的複雜型別。
錯誤 5:巢狀集合缺少註解而被忽略。 沒有 @XmlElementWrapper 時,集合的序列化可能與預期不同,或 JAXB 根本無法正確讀回 XML。
錯誤 6:把配接器套用在集合本身而非元素。 若要透過配接器序列化清單中的元素,請把註解放在元素上,而不是放在集合本身(例如把 @XmlJavaTypeAdapter 標在元素欄位上,或像前述範例那樣標在清單欄位上並指明元素型別)。
GO TO FULL VERSION