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:为字段或 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:缺少无参的公共构造函数。 没有它,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