fastjson2 Mixin 机制详解:给任意类「注入」序列化注解
1. Mixin 是什么
Mixin 是 fastjson2 的「注解外部注入」机制:把A 类(mixin)上的注解,当作 B 类(目标类)的注解来用,运行时织入,不改目标类源码。
一句话:让一个你没动过的类,表现得像被你加了注解一样。
// mixin:一个空壳类,只承载注解staticclassExternalUserMixin{// 字段/方法体无所谓,fastjson2 只读注解}// 注册:把 ExternalUserMixin 的注解贴到不可改的 ExternalUser 上JSON.mixIn(ExternalUser.class,ExternalUserMixin.class);// 此后 ExternalUser 的序列化/反序列化,就像自带了 mixin 上的注解JSON.parseObject(json,ExternalUser.class);灵感来自 Jackson 的@JsonMixIn,fastjson2 在 API 和语义上与之兼容。
2. 为什么需要它
fastjson2 的序列化/反序列化行为是注解驱动的——@JSONType、@JSONField、@JSONCreator
决定一个类怎么被读写。但现实里有大量类你改不了源码:
| 来源 | 例子 |
|---|---|
| JDK 类 | java.time.*、java.util.UUID、java.awt.Point |
| 第三方库 | agentscope 的GenerateOptions、AWS SDK 的各种XxxRequest |
| Lombok 生成物 | @Builder生成的 Builder 类不在源码里,无法标注 |
| 已交付/冻结的类 | 不允许改动、或改动成本太高 |
Mixin 让你在一个你能改的空壳类上写注解,再注册到目标类上,从而在不碰目标源码的前提下定制它的序列化行为。
3. API
3.1 核心 API
// 全局注册(最常用)JSON.mixIn(Class<?>target,Class<?>mixin);// 查询当前注册的 mixinObjectReaderProviderprovider=JSONFactory.getDefaultObjectReaderProvider();Class<?>mixin=provider.getMixIn(target);// 清理所有 mixin 注册(全局)provider.cleanupMixIn();// 单个注册(等价于 JSON.mixIn 的底层)provider.mixIn(target,mixin);签名(2.0.62 实测):
JSON.mixIn(Class<?>, Class<?>>) // static ObjectReaderProvider.mixIn(Class, Class) // instance ObjectReaderProvider.getMixIn(Class) : Class ObjectReaderProvider.cleanupMixIn() // 清空 mixInCache3.2 注册时机
ObjectReader 是按类缓存的。这里有个好消息:mixIn在写入mixInCache的同时,
会主动把目标类从两份 reader 缓存(cache、cacheFieldBased)里移除,因此即便目标类
此前已被反序列化过,注册 mixin 后下一次读取就会按新 mixin 重建 reader,无需手动清缓存或重启:
JSON.parseObject(json,ExternalUser.class);// 此时走默认行为JSON.mixIn(ExternalUser.class,ExternalUserMixin.class);// 注册即作废上面缓存的 readerJSON.parseObject(json,ExternalUser.class);// 这次按 mixin 行为执行唯一边界情况:如果目标类是另一个已缓存的 reader 的内嵌字段类型(例如宿主类
Holder有个ExternalUser字段,Holder的 reader 构建时已内联了旧的ExternalUserreader),那个外层 reader
持有旧引用、不会被本次注册波及。所以生产上仍建议在应用启动阶段集中注册,规避这类时序细节(见 6.1)。
3.3 全局状态警示
JSON.mixIn操作的是全局ObjectReaderProvider,影响整个 JVM 的 fastjson2。
这带来两点注意:
- 作用域是整个 JVM:所有使用 fastjson2 的代码都受影响,包括其他模块、其他线程。
- 测试隔离:在测试里注册 mixin 后要用
cleanupMixIn()还原,避免污染其它用例
(见mixinRescueUnmodifiableBuilder用例的finally块)。
4. Mixin 能做什么(远不止 builder)
Mixin 是一个通用注解注入机制——你在 mixin 上放什么注解,目标类就得到什么行为。
下面列出常见用途。核心认知:mixin 的价值不在某个单一场景,而在于「注解与类解耦」这个能力本身。
4.1 给不可改的 builder 类注入 builder 支持
一个外部、加不了注解的 builder-only 不可变类(全final字段、仅 builder 构造、无无参构造、无 setter),
原本直接反序列化会 NPE。先看这个改不了的目标类长什么样(等价于 agentscope 的GenerateOptions):
// 目标类:外部类,加不了任何 fastjson2 注解staticclassExternalUser{privatefinalStringname;privatefinalintage;ExternalUser(Builderbuilder){this.name=builder.name;this.age=builder.age;}publicStringgetName(){returnname;}publicintgetAge(){returnage;}publicstaticBuilderbuilder(){returnnewBuilder();}/** Lombok 风格 fluent builder:方法名即字段名、无 with 前缀、无 @JSONField。 */publicstaticclassBuilder{privateStringname;privateintage;publicBuildername(Stringname){this.name=name;returnthis;}publicBuilderage(intage){this.age=age;returnthis;}publicExternalUserbuild(){returnnewExternalUser(this);}}}直接JSON.parseObject(json, ExternalUser.class)会走默认的无参构造路径、反射时传入nullbuilder →
构造器内builder.xxx触发 NPE。用 mixin 注入@JSONType(builder=...)即可救活(已实测):
@JSONType(builder=ExternalUser.Builder.class,deserializeFeatures=JSONReader.Feature.SupportSmartMatch)staticclassExternalUserMixin{}// 空壳,只承载注解JSON.mixIn(ExternalUser.class,ExternalUserMixin.class);ExternalUseruser=JSON.parseObject(json,ExternalUser.class);// ✓ 不再 NPE注解里两个要点:
builder = ExternalUser.Builder.class:让反序列化改走 builder 路径,而非默认坏掉的无参构造路径;deserializeFeatures = SupportSmartMatch:因为 Builder 的 fluent 方法name()/age()无with前缀,
必须类级开 SmartMatch 才会被识别为字段。SmartMatch必须写在类级——运行时 vararg 传同样的 feature
无效(builder 的字段发现发生在 ObjectReader 构建期,只读类级注解)。
这是 mixin 最「惊艳」的用法,但只是它能力的一个切面。
4.2 重命名字段
第三方类字段叫userName,你的 JSON 契约要求用name。改不了第三方类?用 mixin:
staticclassUserMixin{@JSONField(name="name")publicStringgetUserName(){returnnull;}// 序列化:userName → 输出 name@JSONField(name="name")publicvoidsetUserName(Stringname){}// 反序列化:name → 填入 userName}JSON.mixIn(ThirdPartyUser.class,UserMixin.class);mixin 本身没有字段、只靠方法签名把注解投射到目标类的对应成员:注解标在 getter 上管序列化字段名,
标在 setter 上管反序列化字段名,故改名要getter 和 setter 都标,否则只单向生效。
(注:这是 mixin 场景的写法。fastjson2 对自有类默认是字段反射驱动的,直接标字段即可,不必绕 getter/setter。)
4.3 忽略字段
第三方类里有敏感/内部字段(如password、internalId),不想进 JSON:
@JSONType(ignores={"password","internalId"})staticclassUserMixin{}JSON.mixIn(ThirdPartyUser.class,UserMixin.class);也可用字段级@JSONField(serialize = false)/(deserialize = false)做单向忽略。
4.4 自定义序列化器 / 反序列化器
给一个无法加@JSONType(serializer=...)的类挂上自定义 codec:
@JSONType(serializer=MoneySerializer.class,deserializer=MoneyDeserializer.class)staticclassMoneyMixin{}JSON.mixIn(Money.class,MoneyMixin.class);适合:金额(分↔元转换)、枚举的扩展码、复数等需要特殊读写逻辑又改不了目标类的场景。
4.5 指定反序列化构造器(@JSONCreator)
一个只有全参构造、没有无参构造、又不是 builder 的不可变类(典型如各种值对象),
可以用 mixin 标注哪个构造器用于反序列化,并用@JSONField(name=...)把 JSON 字段绑到构造器参数:
staticclassAddressMixin{@JSONCreatorpublicAddressMixin(@JSONField(name="address")Stringaddress){}}JSON.mixIn(Address.class,AddressMixin.class);构造器参数靠
@JSONField(name=...)与 JSON 字段对应;mixin 的构造器签名需与目标类的对应构造器一致,
fastjson2 按签名把注解投射过去。本节为用法示意,尚未像 4.1 那样补实测用例。
4.6 统一命名策略
第三方类字段是 camelCase,你想统一输出 snake_case:
@JSONType(naming=PropertyNamingStrategy.SnakeCase)staticclassUserMixin{}JSON.mixIn(ThirdPartyUser.class,UserMixin.class);// userName → user_name, createdAt → created_at4.7 字段排序与格式
@JSONType(orders={"id","name","createTime"})// 输出字段顺序staticclassUserMixin{}@JSONType(format="yyyyMMdd")// 日期格式(类级)staticclassEventMixin{}4.8 用途小结
| 用途 | mixin 上的注解 | 解决的问题 |
|---|---|---|
| 救活 builder-only 不可变类 | @JSONType(builder=...) | 直接反序列化 NPE |
| 重命名字段 | @JSONField(name=...) | JSON 契约与类字段名不一致 |
| 忽略字段 | @JSONType(ignores=...)/@JSONField(serialize=...) | 敏感/内部字段不外泄 |
| 自定义 codec | @JSONType(serializer/deserializer=...) | 特殊读写逻辑 |
| 指定构造器 | @JSONCreator+@JSONField(name=...) | 无无参构造的不可变值对象 |
| 命名策略 | @JSONType(naming=...) | camelCase ↔ snake_case |
| 字段排序/格式 | @JSONType(orders=...)/format | 控制输出 |
5. 设计精髓:注解与类解耦
理解 mixin 的关键,是认识到一个常被忽略的事实:注解不一定要写在被注解的类上。
传统写法里,@JSONField贴在字段上、@JSONType贴在类上——注解和类绑死。
这带来一个隐含限制:「只有类的作者能决定它的序列化行为」。第三方类作者没加注解,
你就只能接受 fastjson2 的默认行为。
Mixin 打破了这个限制,把注解的两重身份拆开:
| 身份 | 传统 | Mixin 后 |
|---|---|---|
| 定义(注解写在哪) | 写在目标类上 | 写在 mixin 空壳类上 |
| 绑定(注解和谁关联) | 编译期固定 | 运行时通过mixIn关联 |
由此带来两个工程价值:
- 开闭原则:对目标类(第三方/冻结)的序列化行为做扩展,而不修改它。
- 关注点分离:领域类保持纯粹(只建模业务),序列化策略集中在 mixin 里。
注意「绑定」不等于「多套配置并行」:
mixInCache是「每个目标类一条记录」的全局 map,
后注册会覆盖前者(见 3.3 的全局性警示),做不到「A 模块 camelCase、B 模块 snake_case」同时并存。
要按环境隔离,需自建独立的JSONFactory/ObjectReaderProvider,已超出本文的JSON.mixIn范畴。
和「字段级
@JSONField(deserializeUsing=...)手写 reader」的区别:
手写 reader 是字段级指定一个完整的ObjectReader,强耦合在宿主类的字段上;
mixin 是类级注入注解,由 fastjson2 原生路径执行,宿主类无感。两者可互补:
复杂场景用 reader 精细控制,常规定制用 mixin 声明式解决。
6. 注意事项与最佳实践
- 全局状态:
JSON.mixIn影响整个 JVM。生产环境建议在应用启动阶段集中注册,
规避「目标类作为外层 reader 内嵌字段」这类时序细节(见 3.2)。 - 注册时机:
mixIn会自动作废目标类已缓存的 reader,故对目标类本身无严格先后要求;
但仍建议集中在启动阶段注册,原因见 3.2 的边界情况。 - getter/setter 对齐:字段改名等配置,mixin 靠方法签名投射,序列化管 getter、反序列化管 setter,两边都要标。
- 测试清理:测试中注册后用
cleanupMixIn()还原,避免污染其他用例。 - mixin 类本身不参与序列化:它是纯注解载体,字段/方法体无关紧要(方法签名要和目标类对应,
fastjson2 靠签名匹配把注解「投射」到目标类的对应成员上)。 - 可替代性:能直接在目标类加注解时,优先直接加;mixin 是「改不了才用」的手段,
不要为了用 mixin 而 mixin(直接注解更直观、无全局状态)。
(END)
