Java 中的容错读取模式:增强 API 的弹性和兼容性
大约 3 分钟
也称为
- 宽松消费者
容错读取设计模式的意图
容错读取模式通过战略性地忽略未识别的元素来增强系统对数据结构更改的弹性,从而促进稳健的 API 设计。
容错读取模式的详细解释以及现实世界中的例子
现实世界中的例子
想象一个邮政系统,它将信件和包裹递送给收件人。在这个系统中,邮递员会递送邮件,无论信封或包裹上是否有额外的信息或贴纸。如果一个包裹有额外的标签或说明,邮政系统无法识别,邮递员会忽略这些信息,只关注地址等基本信息。这种方法确保了即使发件人使用不同的格式或包含不必要的细节,递送过程仍然可以正常运行,这与容错读取模式在软件中通过忽略无法识别的數據元素来保持功能和兼容性的方式类似。
简单来说
利用容错读取模式在服务之间建立稳健且弹性的通信,确保数据兼容性和集成。
健壮性原则 说道
对你自己的行为要保守,对你接受的东西要宽容。
Java 中容错读取模式的编程示例
我们正在将 RainbowFish
对象持久化到文件中。之后需要恢复它们。问题在于 RainbowFish
数据结构是版本化的,并且会随着时间的推移而演变。新版本的 RainbowFish
需要能够恢复旧版本。
这是版本化的 RainbowFish
。注意第二个版本如何引入额外的属性。
@Getter
@RequiredArgsConstructor
public class RainbowFish implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final int age;
private final int lengthMeters;
private final int weightTons;
}
@Getter
public class RainbowFishV2 extends RainbowFish {
@Serial
private static final long serialVersionUID = 1L;
private boolean sleeping;
private boolean hungry;
private boolean angry;
public RainbowFishV2(String name, int age, int lengthMeters, int weightTons) {
super(name, age, lengthMeters, weightTons);
}
public RainbowFishV2(String name, int age, int lengthMeters, int weightTons, boolean sleeping,
boolean hungry, boolean angry) {
this(name, age, lengthMeters, weightTons);
this.sleeping = sleeping;
this.hungry = hungry;
this.angry = angry;
}
}
接下来我们介绍 RainbowFishSerializer
。这是一个实现容错读取模式的类。
@NoArgsConstructor
public final class RainbowFishSerializer {
public static void writeV1(RainbowFish rainbowFish, String filename) throws IOException {
var map = Map.of(
"name", rainbowFish.getName(),
"age", String.format("%d", rainbowFish.getAge()),
"lengthMeters", String.format("%d", rainbowFish.getLengthMeters()),
"weightTons", String.format("%d", rainbowFish.getWeightTons())
);
try (var fileOut = new FileOutputStream(filename);
var objOut = new ObjectOutputStream(fileOut)) {
objOut.writeObject(map);
}
}
public static void writeV2(RainbowFishV2 rainbowFish, String filename) throws IOException {
var map = Map.of(
"name", rainbowFish.getName(),
"age", String.format("%d", rainbowFish.getAge()),
"lengthMeters", String.format("%d", rainbowFish.getLengthMeters()),
"weightTons", String.format("%d", rainbowFish.getWeightTons()),
"angry", Boolean.toString(rainbowFish.getAngry()),
"hungry", Boolean.toString(rainbowFish.getHungry()),
"sleeping", Boolean.toString(rainbowFish.getSleeping())
);
try (var fileOut = new FileOutputStream(filename);
var objOut = new ObjectOutputStream(fileOut)) {
objOut.writeObject(map);
}
}
public static RainbowFish readV1(String filename) throws IOException, ClassNotFoundException {
Map<String, String> map;
try (var fileIn = new FileInputStream(filename);
var objIn = new ObjectInputStream(fileIn)) {
map = (Map<String, String>) objIn.readObject();
}
return new RainbowFish(
map.get("name"),
Integer.parseInt(map.get("age")),
Integer.parseInt(map.get("lengthMeters")),
Integer.parseInt(map.get("weightTons"))
);
}
}
最后,这是完整的示例。
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Write V1
var fishV1 = new RainbowFish("Zed", 10, 11, 12);
LOGGER.info("fishV1 name={} age={} length={} weight={}", fishV1.getName(),
fishV1.getAge(), fishV1.getLengthMeters(), fishV1.getWeightTons());
RainbowFishSerializer.writeV1(fishV1, "fish1.out");
// Read V1
var deserializedRainbowFishV1 = RainbowFishSerializer.readV1("fish1.out");
LOGGER.info("deserializedFishV1 name={} age={} length={} weight={}",
deserializedRainbowFishV1.getName(), deserializedRainbowFishV1.getAge(),
deserializedRainbowFishV1.getLengthMeters(), deserializedRainbowFishV1.getWeightTons());
// Write V2
var fishV2 = new RainbowFishV2("Scar", 5, 12, 15, true, true, true);
LOGGER.info(
"fishV2 name={} age={} length={} weight={} sleeping={} hungry={} angry={}",
fishV2.getName(), fishV2.getAge(), fishV2.getLengthMeters(), fishV2.getWeightTons(),
fishV2.isHungry(), fishV2.isAngry(), fishV2.isSleeping());
RainbowFishSerializer.writeV2(fishV2, "fish2.out");
// Read V2 with V1 method
var deserializedFishV2 = RainbowFishSerializer.readV1("fish2.out");
LOGGER.info("deserializedFishV2 name={} age={} length={} weight={}",
deserializedFishV2.getName(), deserializedFishV2.getAge(),
deserializedFishV2.getLengthMeters(), deserializedFishV2.getWeightTons());
}
程序输出
15:38:00.602 [main] INFO com.iluwatar.tolerantreader.App -- fishV1 name=Zed age=10 length=11 weight=12
15:38:00.618 [main] INFO com.iluwatar.tolerantreader.App -- deserializedFishV1 name=Zed age=10 length=11 weight=12
15:38:00.618 [main] INFO com.iluwatar.tolerantreader.App -- fishV2 name=Scar age=5 length=12 weight=15 sleeping=true hungry=true angry=true
15:38:00.619 [main] INFO com.iluwatar.tolerantreader.App -- deserializedFishV2 name=Scar age=5 length=12 weight=15
何时在 Java 中使用容错读取模式
- 当您的系统从不断变化的外部来源消费数据时,请应用容错读取模式,以保持效率和数据完整性。
- 适用于 API 设计需要向后兼容性的情况。
- 适用于不同系统交换数据并独立演变的集成场景。
Java 中容错读取模式的现实世界应用
- 跳过未知元素的 JSON 或 XML 解析器。
- 微服务架构中与多个服务版本交互的 API 客户端。
容错读取模式的优点和权衡
优点
- 提高系统的稳健性和灵活性。
- 允许分布式系统中生产者和消费者的独立演变。
- 通过实现向后兼容性简化版本控制。
权衡
- 如果忽略了重要数据,可能会导致静默失败。
- 由于缺少或无法识别的數據,可能会使调试和跟踪问题变得复杂。
相关的 Java 设计模式
- 适配器:这两种模式都处理数据转换和集成,但适配器模式侧重于转换接口,而容错读取模式则侧重于忽略无法识别的數據。
- 外观:简化与复杂系统的交互,类似于容错读取模式通过忽略无关数据来简化数据消费。
- 策略:可以与容错读取模式结合使用,以动态地在不同的数据处理策略之间切换。