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 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 不支持的类型。 典型示例——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