调研结论:Cangjie 反射对字段/prop/方法的声明类严格校验(declaringClass != instance 类型) 堵死运行时方案;宏无法编译期查询父类字段;编译器禁止子类遮蔽父类字段。 唯一可行通路(已实测):宏标注父类生成 exportJsonFields/importJsonFields 静态方法, 序列化库遍历 superClass 链通过静态方法反射调用(静态方法无实例声明类检查, 子类实例可直接作父类类型参数)。
17 KiB
方案:父类字段序列化支持(宏 + 静态导出方法)
状态:待评审 目标仓库:simapi-serialization 关联问题:Cangjie 反射无法通过父类字段句柄读写子类实例的值,导致继承的父类字段无法序列化/反序列化。
1. 问题与调研结论
1.1 现象
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 硬编码):
// 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 唯一的可行通路(已实测验证)
静态方法反射调用没有「实例声明类检查」,且子类实例可直接作为父类类型参数传入:
// 父类上定义一个静态方法,参数类型是父类
public static func exportJsonFields(instance: BaseUser): HashMap<String, Any> {
var m = HashMap<String, Any>()
m["_id"] = instance._id // 静态类型访问,编译期就知道字段存在
m
}
// 反射调用(核心验证通过):
let f = TypeInfo.of<BaseUser>().getStaticFunction("exportJsonFields", [TypeInfo.of<BaseUser>()])
let args: Array<Any> = [child] // Child 实例作为 BaseUser 参数 ✓
let result = f.apply(TypeInfo.of<BaseUser>(), 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 宏定义
// 文件: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 生成的目标代码
对如下父类:
@JsonExportFields
open class BaseUser {
public var _id: String = ""
public var _createdAt: String = ""
@JsonIgnore
public var _temp: String = ""
}
宏展开后等价于:
open class BaseUser {
public var _id: String = ""
public var _createdAt: String = ""
@JsonIgnore
public var _temp: String = ""
/// 导出:字段名 → 值(静态类型访问)
public static func exportJsonFields(instance: BaseUser): HashMap<String, Any> {
var m = HashMap<String, Any>()
m["_id"] = instance._id
m["_createdAt"] = instance._createdAt
// _temp 被 @JsonIgnore 跳过
m
}
/// 导入:按 JSON 键回填字段(静态类型赋值)
public static func importJsonFields(instance: BaseUser, json: HashMap<String, Any>): 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 链向上逐个检查约定方法:
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<Any> = [value] // 子类实例作父类参数
(f.apply(p, args) as HashMap<String, Any>) ?? HashMap<String, Any>()
} catch (_: Exception) {
HashMap<String, Any>() // 无宏的父类 → 空,继续上层
}
for ((k, v) in parentFields) {
obj.put(k, writeValue(v, options, depth + 1))
}
parent = p.superClass
}
obj
}
要点:
- 静态方法返回的是已序列化的字段值(Any),直接
writeValue二次处理即可(嵌套对象/集合/枚举都会正确递归); - 异常即「无此方法」,静默继续向上,无宏的类完全不受影响;
- 需加缓存:父类→是否含约定方法,避免每次反射查找(性能)。
4.2 JsonReader(反序列化侧)
readObject 中,构造实例并写完子类字段后,同样沿 superClass 链调用约定方法回填:
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<HashMap<String, Any>>()])
} catch (_: Exception) {
// 无宏的父类 → 跳过
parent = p.superClass
continue
}
// 只把父类认识的键传给导入方法(避免把子类字段塞给父类)
let parentKeys = exportKeysOf(p) // 复用 exportJsonFields 的键名集合(缓存)
var pjson = HashMap<String, Any>()
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 缓存:
// 类型 → 是否有 exportJsonFields/importJsonFields 约定方法(Bool)
private static let _parentExportCache = HashMap<String, Bool>()
private static let _parentImportCache = HashMap<String, Bool>()
5. 使用示例
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<User>(json)
// u2._id == "u-1" ✓ u2._name == "alice" ✓
}
6. 测试计划
在 src/json/ 下新增测试(复用现有 JsonSerializer_test.cj 的测试基建):
| 用例 | 断言 |
|---|---|
| 父类字段序列化 | 输出包含 _id/_createdAt,且 @JsonIgnore 父类字段不输出 |
| 父类字段反序列化 | Deserialize<User> 后父类字段值正确 |
| 多层继承 | 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<String, Any> 的类型转换 |
静态方法反射 apply 返回 Any,需 ?? 兜底;若反射返回类型严格匹配失败,可在宏生成时返回 Array<(String, Any)> 或专用 DTO 规避 |
| 4 | 父类字段顺序 | 继承链遍历顺序(父类在前还是后)影响 JSON 键顺序;JSON 对象键无序,不构成功能问题 |
| 5 | 与现有「父类字段不序列化」测试冲突 | 现有测试 objectSerialize 断言 _id/_createdAt 不输出——该断言需在宏方案落地后反转/条件化(无宏时仍不输出,有宏时输出) |
8. 备选方案对比
| 方案 | 可行性 | 改动量 | 说明 |
|---|---|---|---|
| 宏标注父类 + 静态导出方法(本方案) | ✅ 已验证 | 中 | 自动、向后兼容、无反射限制 |
| 序列化库运行时遍历继承链 + 反射读父类字段 | ❌ | — | getValue 声明类严格校验,直接抛异常 |
| 宏把父类字段复制到子类 | ❌ | — | 编译器禁止字段遮蔽 |
| 用户在子类手写父类字段(复制粘贴) | ✅ | 小但人工 | 破坏继承语义、易漏改;仅作为临时 workaround |
| 序列化库要求父类字段改用 prop + 方法反射 | ❌ | — | 方法反射同样有声明类校验(已实测) |
9. 实施步骤(评审通过后)
- 新建宏文件
src/json/JsonExportFieldsMacro.cj(或独立宏包),实现JsonExportFields宏; - 验证 cjpm 对宏包的编译流程(风险 #1),必要时调整项目结构;
- 改
JsonWriter.writeObject:superClass 链调用exportJsonFields; - 改
JsonReader.readObject:superClass 链调用importJsonFields; ReflectionCache增加约定方法存在性缓存;- 新增 §6 测试用例;调整现有
objectSerialize断言(风险 #5); cjpm test全绿;- 提交并推送。