多版本支持
多版本支持 0.1.0
介绍
协议是会进化的。版本升级时,往往只是部分字段发生变化:
- 某字段在旧版本中是
u8,新版本变成了u16 - 某字段在旧版本中以 GBK 编码,新版本改用 UTF-8
- 某字段干脆就是新版本才有的
- ……
若为每个版本维护一套独立的 Entity 类,代码将迅速膨胀,版本之间的差异淹没在重复的 boilerplate 中。
@XtreamField 的 version 属性便是为此而生——在 同一 Entity 类 中,通过 同一字段 上的多个注解声明,精确控制每个版本下该字段的编解码行为。
基本用法
@XtreamField 及其所有 Preset 别名注解(如 @Preset.RustStyle.u8、@Preset.JtStyle.Dword 等),都支持 version 属性:
// 完整类定义参见下方“完整示例”
class VersionedEntity {
@Preset.RustStyle.u32(desc = "用户ID")
private Long id;
// 默认版本(匹配所有版本):UTF-8 编码
@Preset.RustStyle.str(prependLengthFieldType = PrependLengthFieldType.u8,
desc = "用户名(UTF-8)")
// 仅 version=1,2:GBK 编码
@Preset.RustStyle.str(prependLengthFieldType = PrependLengthFieldType.u8,
desc = "用户名(GBK)",
version = {1, 2},
charset = XtreamConstants.CHARSET_NAME_GBK)
private String name;
}上例中,name 字段声明了两个 @Preset.RustStyle.str 注解:
- 第一个没有指定
version,默认值{ALL_VERSION},作为兜底 - 第二个指定了
version = {1, 2},表示版本 1 和 2 使用 GBK 编码
在测试中通过 doCodecTest(version, ...) 的版本参数控制使用哪个注解:
class DocTest {
void test() {
// version=1 匹配 version={1,2} → GBK 编码
doCodecTest(1, entity, (source, hex, decoded) -> {
});
// version=ALL_VERSION 匹配默认注解 → UTF-8 编码
doCodecTest(XtreamField.ALL_VERSION, entity, (source, hex, decoded) -> {
});
}
}ALL_VERSION 常量
ALL_VERSION 是 @XtreamField 中定义的常量,值为 Integer.MIN_VALUE:
@interface XtreamField {
int ALL_VERSION = Integer.MIN_VALUE;
}未指定 version 的注解默认值为 {ALL_VERSION},表示匹配所有版本。当没有精确匹配时,框架会使用 ALL_VERSION 的注解作为兜底。
版本匹配规则
框架在编解码时,按照以下规则为每个字段选择生效的注解:
- 精确匹配:
@XtreamField或其别名注解的version[]数组中包含目标版本 → 使用该注解 - 默认兜底:无精确匹配时,如果存在
version = {ALL_VERSION}的注解 → 使用该注解 - 忽略字段:既无精确匹配也无默认注解 → 该字段在当前版本下被跳过(不编码、不解码)
目标版本=2
字段上的注解:
@Preset.RustStyle.str(version = {1}) → 不匹配
@Preset.RustStyle.str(version = {ALL_VERSION}) → 匹配(兜底)
→ 最终使用兜底注解目标版本=3
字段上的注解:
@Preset.RustStyle.str(version = {1, 2}) → 不匹配
@Preset.RustStyle.str(version = {ALL_VERSION}) → 匹配(兜底)
→ 最终使用兜底注解目标版本=2
字段上的注解:
@Preset.RustStyle.str(version = {2}) → 精确匹配
→ 最终使用 version={2} 的注解@Repeatable 在同一字段上声明多个版本
@XtreamField 以及所有 Preset 别名注解(如 @Preset.RustStyle.str)都标注了 @Repeatable,允许在同一个字段上重复使用。
这是实现多版本的核心机制——每个注解声明一种版本的编解码配置,框架根据运行时版本自动选择。
不同编码格式
class MultiVersionEntity {
// V1: GBK 编码
@Preset.RustStyle.str(prependLengthFieldType = PrependLengthFieldType.u8,
charset = XtreamConstants.CHARSET_NAME_GBK,
version = {1})
// V2: UTF-8 编码
@Preset.RustStyle.str(prependLengthFieldType = PrependLengthFieldType.u8,
charset = XtreamConstants.CHARSET_NAME_UTF8,
version = {2})
private String username;
}不同类型
class MultiVersionEntity {
// V1: u8
@Preset.RustStyle.u8(version = {1})
// V2: u16
@Preset.RustStyle.u16(version = {2})
private int status;
}新版本新增字段
新版本的字段在旧版本中不存在,只需不为旧版本声明注解即可:
class MultiVersionEntity {
// 仅在 version≥2 时编解码
@Preset.RustStyle.str(prependLengthFieldType = PrependLengthFieldType.u8,
version = {2})
private String newField;
}当以 version=1 编解码时,newField 会被忽略(规则 3)。
Preset 别名注解中的 version
所有 Preset 别名注解都通过 Spring 的 @AliasFor 将 version 属性委托给 @XtreamField#version:
@interface SomeAlias {
@AliasFor(annotation = XtreamField.class, attribute = "version")
int[] version() default {XtreamField.ALL_VERSION};
}这意味着以下写法完全等价:
class VersionedEntity {
// 直接使用 @XtreamField
@XtreamField(version = {1} /*, ... 其他属性 ... */)
private int status;
// 使用 Preset 别名
@Preset.RustStyle.u8(version = {1} /*, ... 其他属性 ... */)
private int status;
}可用的 Preset 注解族见 @Preset 注解族。
@DerivedField 中的 version
@DerivedField 同样支持 version 属性,可在不同版本下使用不同的派生逻辑。详见 @DerivedField 多版本支持。
完整示例
以下测试类综合演示了上述所有场景——不同编码格式、不同类型、新版本新增字段,以及 ALL_VERSION 兜底行为:
/// 演示 `@XtreamField#version` 的多版本编解码。
///
/// V1 与 V2 共用同一 Entity 结构,但部分字段在不同版本下有不同编解码行为。
///
/// @author opencode (AI)
/// @since 0.6.0
@ReferencedByDocs("guide/core/annotation-driven/multi-version.md")
class MultiVersionCodecTest extends BaseEntityCodecTest {
public interface Versions {
int V1 = 1;
int V2 = 2;
}
@Getter
@Setter
@Accessors(chain = true)
public static class VersionedEntity {
// 两版本共有:u32,行为一致
@Preset.RustStyle.u32(desc = "用户ID")
private Long id;
// V1: GBK 编码;V2: UTF-8 编码
@Preset.RustStyle.str(
prependLengthFieldType = PrependLengthFieldType.u8,
version = {Versions.V1},
desc = "用户名(V1 GBK)",
charset = XtreamConstants.CHARSET_NAME_GBK
)
@Preset.RustStyle.str(
prependLengthFieldType = PrependLengthFieldType.u8,
version = {Versions.V2},
desc = "用户名(V2 UTF-8)",
charset = XtreamConstants.CHARSET_NAME_UTF8
)
private String name;
// V1: u8;V2: u16;其余版本默认 u32
@Preset.RustStyle.u8(desc = "年龄(V1 u8)", version = {Versions.V1})
@Preset.RustStyle.u16(desc = "年龄(V2 u16)", version = {Versions.V2})
@Preset.RustStyle.u32(desc = "年龄(默认 u32)")
private long age;
// 仅 V2 有
@Preset.RustStyle.str(
prependLengthFieldType = PrependLengthFieldType.u8,
desc = "邮箱(仅V2)",
version = {Versions.V2}
)
private String email;
}
@Test
void testV1() {
final VersionedEntity entity = new VersionedEntity()
.setId(100L)
.setName("张三")
.setAge(25);
doCodecTest(Versions.V1, entity, (source, hex, decoded) -> {
assertEquals(Long.valueOf(100L), decoded.id);
assertEquals("张三", decoded.name);
assertEquals(25, decoded.age);
// V1 无 email 字段,应为 null
assertNull(decoded.email);
}, false);
}
@Test
void testV2() {
final VersionedEntity entity = new VersionedEntity()
.setId(100L)
.setName("张三")
.setAge(25)
.setEmail("zhangsan@example.com");
doCodecTest(Versions.V2, entity, (source, hex, decoded) -> {
assertEquals(Long.valueOf(100L), decoded.id);
assertEquals("张三", decoded.name);
assertEquals(25, decoded.age);
assertEquals("zhangsan@example.com", decoded.email);
}, false);
}
// 验证 version=ALL_VERSION 时,没有指定 ALL_VERSION 的字段回退到默认注解
@Test
void testAllVersionFallback() {
final VersionedEntity entity = new VersionedEntity()
.setId(100L)
.setName("张三")
.setAge(25);
// ALL_VERSION 不精确匹配 V1 或 V2
// - name 只有 V1/V2 注解 → 无兜底 → 被跳过
// - age 有 ALL_VERSION 默认 u32 注解 → 正常编解码
// - id 默认 ALL_VERSION → 正常编解码
doCodecTest(XtreamField.ALL_VERSION, entity, (source, hex, decoded) -> {
assertEquals(Long.valueOf(100L), decoded.id);
assertNull(decoded.name);
assertNull(decoded.email);
assertEquals(25, decoded.age);
}, false);
}
}注意事项
version是int[]:每个注解可声明匹配多个版本,如version = {1, 2, 3}ALL_VERSION兜底:建议始终保留一个version = {ALL_VERSION}(即不指定 version)的注解,作为未匹配版本的默认行为@Repeatable依赖:同一字段上的多个版本声明依赖注解的@Repeatable元注解,确保使用正确- 版本号由调用方传入:
EntityCodec.encode(version, ...)/EntityCodec.decode(version, ...)的第一个参数即是版本号,框架据此匹配