diff --git a/docs/父类字段序列化方案.md b/docs/父类字段序列化方案.md new file mode 100644 index 0000000..85f7b23 --- /dev/null +++ b/docs/父类字段序列化方案.md @@ -0,0 +1,410 @@ +# 方案:父类字段序列化支持(宏 + 静态导出方法) + +> 状态:待评审 +> 目标仓库:simapi-serialization +> 关联问题:Cangjie 反射无法通过父类字段句柄读写子类实例的值,导致继承的父类字段无法序列化/反序列化。 + +--- + +## 1. 问题与调研结论 + +### 1.1 现象 + +```cangjie +open class BaseUser { + public var _id: String = "" + public var _createdAt: String = "" +} + +class User <: BaseUser { + public var _name: String = "" +} + +let u = User() +JsonSerializer.Serialize(u) +// 当前输出只含 User 自身字段:{ "_name": "alice" } +// 期望输出包含父类字段:{ "_id": "...", "_createdAt": "...", "_name": "..." } +``` + +### 1.2 根因(已实测确认,cjc 1.1.3) + +Cangjie 标准库 `std.reflect` 的读写句柄全部带**声明类严格校验**(`variable_info_cjnative.cj` 硬编码): + +```cangjie +// InstanceVariableInfo.getValue / setValue +func checkDeclaringClassType(instance: Any): Unit { + let declaringClassTypeInfo = getFieldDeclaringClassType(_declaringTypeInfo) + let instanceTypeInfo = TypeInfo.of(instance) + if (declaringClassTypeInfo != instanceTypeInfo) { + throw IllegalTypeException( + "The input instance should be \"${declaringClassTypeInfo}\", but now it`s \"${instanceTypeInfo}\"") + } +} +``` + +`declaringClassTypeInfo != TypeInfo.of(instance)` 是**严格相等**,不是子类型判断。实测三类句柄全部被堵死: + +| 句柄 | 在子类实例上读写父类成员 | 实测结果 | +|---|---|---| +| `InstanceVariableInfo`(var 字段) | `baseFields.getValue(child)` | ❌ IllegalTypeException | +| `InstancePropertyInfo`(prop) | `baseProps.getValue(child)` | ❌ IllegalTypeException | +| `InstanceFunctionInfo`(方法) | `baseFunc.apply(child, args)` | ❌ IllegalTypeException | + +### 1.3 两个被否掉的路线(均已实测) + +**路线 A:宏把父类字段「复制」到子类声明里** + +不可行,Cangjie 编译器**禁止字段遮蔽**: + +``` +error: the variable '_id' must not shadow a member variable of the supertype +``` + +即使宏能在子类里生成同名同类型字段,编译器直接拒绝编译。 + +**路线 B:宏在编译期查询父类的字段列表,自动生成代码** + +不可行,Cangjie 宏是**纯语法层**操作: + +- 宏的输入只有被标注声明自身的 Tokens(如 `class User <: BaseUser {...}` 这段代码); +- 宏没有编译期类型查询 API(无类似 Rust proc_macro 的 TypeInfo 上下文); +- `std.ast` 的 `Position` 只含 `fileID/line/column`,无源码读取 API; +- 因此宏**看不到父类声明里的字段**(父类可能在别的文件/包)。 + +### 1.4 唯一的可行通路(已实测验证) + +**静态方法反射调用没有「实例声明类检查」**,且**子类实例可直接作为父类类型参数传入**: + +```cangjie +// 父类上定义一个静态方法,参数类型是父类 +public static func exportJsonFields(instance: BaseUser): HashMap { + var m = HashMap() + m["_id"] = instance._id // 静态类型访问,编译期就知道字段存在 + m +} + +// 反射调用(核心验证通过): +let f = TypeInfo.of().getStaticFunction("exportJsonFields", [TypeInfo.of()]) +let args: Array = [child] // Child 实例作为 BaseUser 参数 ✓ +let result = f.apply(TypeInfo.of(), args) // ✓ 调用成功 +``` + +原理:`exportJsonFields` 内部用**静态类型** `BaseUser` 访问 `_id`,走的是普通字段访问(非反射),完全合法;外部通过静态方法反射调用,`checkStaticDeclaringType` 只校验声明的类(用 `thisType` 参数),不校验实例的运行时类型。 + +--- + +## 2. 方案总览 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 宏 @JsonExportFields(标注在父类上) │ +│ → 编译期解析父类声明的 var 字段 │ +│ → 自动生成两个静态方法(静态类型访问,无反射检查): │ +│ exportJsonFields(instance: Parent) │ +│ importJsonFields(instance: Parent, jsonObj) │ +└─────────────────────────────────────────────────────────┘ + ↓ 运行时 +┌─────────────────────────────────────────────────────────┐ +│ 序列化库 JsonWriter / JsonReader │ +│ → 遍历 superClass 链(Child → Parent → ... → Object) │ +│ → 每个父类若存在约定静态方法则调用并合并字段 │ +│ → 无宏的类行为不变(向后兼容) │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.1 为什么宏标注在「父类」而不是「子类」 + +宏只能看到自己标注的那份声明的 Tokens。父类自己知道自己有哪些字段,所以宏标注在父类上,才能解析出字段清单并生成代码。子类不需要任何改动,运行时靠继承链自动找到父类的导出方法。 + +### 2.2 为什么能绕过反射限制 + +- 导出方法内部:`instance._id` 是静态类型字段访问(编译期解析),不是反射,无任何运行时检查; +- 导出方法外部:通过**静态方法反射**调用,静态方法没有「实例声明类」校验,只校验声明类(用 `thisType` 参数),子类实例天然是父类类型的合法值。 + +--- + +## 3. 宏设计 + +### 3.1 宏定义 + +```cangjie +// 文件:src/json/JsonExportFieldsMacro.cj +// 包:simapi_serialization.json(宏包需单独编译,见 3.3) + +macro package simapi_serialization.json + +import std.ast.* +import std.collection.* + +/// 标注在父类上:自动生成父类字段的 JSON 导出/导入静态方法 +public macro JsonExportFields(input: Tokens): Tokens { + // 1. 解析输入的 ClassDecl + // 2. 收集 public var 字段(跳过 @JsonIgnore) + // 3. 生成 exportJsonFields / importJsonFields 两个静态方法 + // 4. 拼回原类声明 + 新方法,返回 Tokens +} +``` + +### 3.2 生成的目标代码 + +对如下父类: + +```cangjie +@JsonExportFields +open class BaseUser { + public var _id: String = "" + public var _createdAt: String = "" + @JsonIgnore + public var _temp: String = "" +} +``` + +宏展开后等价于: + +```cangjie +open class BaseUser { + public var _id: String = "" + public var _createdAt: String = "" + @JsonIgnore + public var _temp: String = "" + + /// 导出:字段名 → 值(静态类型访问) + public static func exportJsonFields(instance: BaseUser): HashMap { + var m = HashMap() + m["_id"] = instance._id + m["_createdAt"] = instance._createdAt + // _temp 被 @JsonIgnore 跳过 + m + } + + /// 导入:按 JSON 键回填字段(静态类型赋值) + public static func importJsonFields(instance: BaseUser, json: HashMap): Unit { + for ((k, v) in json) { + match (k) { + case "_id" => + if (let s: String <- v) { instance._id = s } + case "_createdAt" => + if (let s: String <- v) { instance._createdAt = s } + case _ => () // 未知键忽略 + } + } + } +} +``` + +### 3.3 宏包的编译方式 + +Cangjie 宏需要两步编译(`cjc --compile-macro` 先编宏,再编使用宏的代码)。cjpm 对宏的支持: + +- 方案一:宏定义放在独立包(如 `simapi_serialization.macros`),`cjpm` 中通过依赖引入,宏包内 `cjpm.toml` 声明为宏包; +- 方案二:宏定义与库同包,使用 `--compile-macro` 先行编译——需要确认 cjpm 是否自动处理。 + +> ⚠️ 待验证项:cjpm 对同包宏的编译顺序支持(见 §7 风险)。 + +### 3.4 宏需要处理的细节 + +| 细节 | 处理 | +|---|---| +| 字段类型 | 用 `TypeInfo.get(字段类型名)` 在运行时做类型转换(import 侧) | +| `@JsonPropertyName` | export 键用注解名;import 匹配也用注解名 | +| `@JsonIgnore` | 直接跳过,不生成导出/导入语句 | +| `let`(不可变)字段 | 只导出,不导入(导入侧跳过) | +| 无字段 | 生成空方法(返回空 map / 空操作),保证约定方法一定存在 | +| 命名冲突 | 若用户已手写同名静态方法,宏应报错或跳过(设计取舍:报错更安全) | +| 泛型父类 | v1 不支持,文档注明(反射对泛型类型参数处理复杂) | +| 多层继承 | 每个父类各自标注宏,序列化库逐层调用 | + +--- + +## 4. 序列化库改动 + +### 4.1 JsonWriter(序列化侧) + +`writeObject` 中,反射写完子类自身字段后,**沿 superClass 链向上**逐个检查约定方法: + +```cangjie +private static func writeObject(value: Any, ct: ClassTypeInfo, options: JsonOption, depth: Int64): JsonValue { + let obj = JsonObject() + // 1) 子类自身字段(现状不变) + let fields = ReflectionCache.getFields(ct, options) + for (f in fields) { + if (f.ignore) { continue } + let raw = f.variable.getOrThrow().getValue(value) + obj.put(f.jsonName, writeValue(raw, options, depth + 1)) + } + // 2) 父类字段(新增):沿继承链调用约定静态方法 + var parent = ct.superClass + while (let Some(p) <- parent) { + let parentFields = try { + let f = p.getStaticFunction("exportJsonFields", [p]) + let args: Array = [value] // 子类实例作父类参数 + (f.apply(p, args) as HashMap) ?? HashMap() + } catch (_: Exception) { + HashMap() // 无宏的父类 → 空,继续上层 + } + for ((k, v) in parentFields) { + obj.put(k, writeValue(v, options, depth + 1)) + } + parent = p.superClass + } + obj +} +``` + +要点: +- 静态方法返回的是**已序列化的字段值**(Any),直接 `writeValue` 二次处理即可(嵌套对象/集合/枚举都会正确递归); +- 异常即「无此方法」,静默继续向上,**无宏的类完全不受影响**; +- 需加缓存:父类→是否含约定方法,避免每次反射查找(性能)。 + +### 4.2 JsonReader(反序列化侧) + +`readObject` 中,构造实例并写完子类字段后,同样沿 superClass 链调用约定方法回填: + +```cangjie +private static func readObject(value: JsonValue, ct: ClassTypeInfo, options: JsonOption, depth: Int64): Any { + let instance = ct.construct([]) + // 1) 子类自身字段(现状不变) + let fields = ReflectionCache.getFields(ct, options) + let jobj = value.asObject() + for (f in fields) { + if (f.ignore || !f.mutable) { continue } + match (jobj.get(f.jsonName)) { + case Some(jv) => + if (jv is JsonNull) { continue } + let fieldVal = readValue(jv, f.typeInfo.getOrThrow(), options, depth + 1) + f.variable.getOrThrow().setValue(instance, fieldVal) + case None => () + } + } + // 2) 父类字段(新增):收集 JSON 中父类能识别的键 + var parent = ct.superClass + while (let Some(p) <- parent) { + let importFn = try { + p.getStaticFunction("importJsonFields", [p, TypeInfo.of>()]) + } catch (_: Exception) { + // 无宏的父类 → 跳过 + parent = p.superClass + continue + } + // 只把父类认识的键传给导入方法(避免把子类字段塞给父类) + let parentKeys = exportKeysOf(p) // 复用 exportJsonFields 的键名集合(缓存) + var pjson = HashMap() + for (k in parentKeys) { + match (jobj.get(k)) { + case Some(jv) => pjson[k] = jsonToAny(jv) + case None => () + } + } + importFn.apply(p, [instance, pjson]) + parent = p.superClass + } + instance +} +``` + +要点: +- 导入方法只回填它认识的键,未知键忽略(宏已生成 `case _ => ()`); +- 传给导入方法的 JSON 值用 `jsonToAny` 还原为通用 Any,由宏内部做 `as` 转换; +- null 值:宏侧 `if (let s: String <- v)` 天然跳过 null,保持「None/默认值」语义。 + +### 4.3 缓存 + +新增 `ReflectionCache` 缓存: + +```cangjie +// 类型 → 是否有 exportJsonFields/importJsonFields 约定方法(Bool) +private static let _parentExportCache = HashMap() +private static let _parentImportCache = HashMap() +``` + +--- + +## 5. 使用示例 + +```cangjie +import simapi_serialization.* +import simapi_serialization.json.JsonExportFields // 引入宏 + +// 父类标注宏 +@JsonExportFields +open class BaseUser { + public var _id: String = "" + public var _createdAt: String = "" +} + +// 子类零改动 +class User <: BaseUser { + public var _name: String = "" + public var _age: Int64 = 0 +} + +main() { + let u = User() + u._id = "u-1" + u._createdAt = "2025-01-01" + u._name = "alice" + u._age = 30 + + let json = JsonSerializer.Serialize(u) + // 输出(字段顺序:子类自身字段 + 父类字段): + // { "_name": "alice", "_age": 30, "_id": "u-1", "_createdAt": "2025-01-01" } + + let u2 = JsonSerializer.Deserialize(json) + // u2._id == "u-1" ✓ u2._name == "alice" ✓ +} +``` + +--- + +## 6. 测试计划 + +在 `src/json/` 下新增测试(复用现有 `JsonSerializer_test.cj` 的测试基建): + +| 用例 | 断言 | +|---|---| +| 父类字段序列化 | 输出包含 `_id`/`_createdAt`,且 `@JsonIgnore` 父类字段不输出 | +| 父类字段反序列化 | `Deserialize` 后父类字段值正确 | +| 多层继承 | GrandParent → Parent → Child 三层字段全部输出 | +| 无宏的类 | 行为与现状完全一致(回归) | +| 子类字段与父类字段重名 | 子类优先(输出一份,值取子类的) | +| 父类嵌套对象字段 | 父类字段值是对象时正确递归 | +| 父类字段 null | 导入时保持默认值 | +| 与 @JsonPropertyName 组合 | 父类字段用注解名输出/匹配 | +| 顶层序列化父类类型实例 | `Serialize(BaseUser 实例)` 直接可用 | + +--- + +## 7. 风险与待验证项 + +| # | 风险 | 说明/对策 | +|---|---|---| +| 1 | **cjpm 对宏包的支持** | Cangjie 宏需 `cjc --compile-macro` 先行编译;需验证 cjpm 依赖系统能否自动处理宏包(若不能,改独立宏包目录 + 手动编译脚本,或文档说明编译顺序) | +| 2 | 宏解析字段的复杂度 | 需要处理 `public var`/`public let`/注解/默认值表达式;用 `parseDecl` 解析后遍历 `ClassDecl.members` 中的 `VarDecl` | +| 3 | `as HashMap` 的类型转换 | 静态方法反射 `apply` 返回 `Any`,需 `??` 兜底;若反射返回类型严格匹配失败,可在宏生成时返回 `Array<(String, Any)>` 或专用 DTO 规避 | +| 4 | 父类字段顺序 | 继承链遍历顺序(父类在前还是后)影响 JSON 键顺序;JSON 对象键无序,不构成功能问题 | +| 5 | 与现有「父类字段不序列化」测试冲突 | 现有测试 `objectSerialize` 断言 `_id`/`_createdAt` 不输出——**该断言需在宏方案落地后反转/条件化**(无宏时仍不输出,有宏时输出) | + +--- + +## 8. 备选方案对比 + +| 方案 | 可行性 | 改动量 | 说明 | +|---|---|---|---| +| **宏标注父类 + 静态导出方法(本方案)** | ✅ 已验证 | 中 | 自动、向后兼容、无反射限制 | +| 序列化库运行时遍历继承链 + 反射读父类字段 | ❌ | — | `getValue` 声明类严格校验,直接抛异常 | +| 宏把父类字段复制到子类 | ❌ | — | 编译器禁止字段遮蔽 | +| 用户在子类手写父类字段(复制粘贴) | ✅ | 小但人工 | 破坏继承语义、易漏改;仅作为临时 workaround | +| 序列化库要求父类字段改用 prop + 方法反射 | ❌ | — | 方法反射同样有声明类校验(已实测) | + +--- + +## 9. 实施步骤(评审通过后) + +1. 新建宏文件 `src/json/JsonExportFieldsMacro.cj`(或独立宏包),实现 `JsonExportFields` 宏; +2. 验证 cjpm 对宏包的编译流程(风险 #1),必要时调整项目结构; +3. 改 `JsonWriter.writeObject`:superClass 链调用 `exportJsonFields`; +4. 改 `JsonReader.readObject`:superClass 链调用 `importJsonFields`; +5. `ReflectionCache` 增加约定方法存在性缓存; +6. 新增 §6 测试用例;调整现有 `objectSerialize` 断言(风险 #5); +7. `cjpm test` 全绿; +8. 提交并推送。