当前位置: 首页 > news >正文

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.UUIDjava.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() // 清空 mixInCache

3.2 注册时机

ObjectReader 是按类缓存的。这里有个好消息:mixIn在写入mixInCache的同时,
主动把目标类从两份 reader 缓存(cachecacheFieldBased)里移除,因此即便目标类
此前已被反序列化过,注册 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。
这带来两点注意:

  1. 作用域是整个 JVM:所有使用 fastjson2 的代码都受影响,包括其他模块、其他线程。
  2. 测试隔离:在测试里注册 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 忽略字段

第三方类里有敏感/内部字段(如passwordinternalId),不想进 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_at

4.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关联

由此带来两个工程价值:

  1. 开闭原则:对目标类(第三方/冻结)的序列化行为做扩展,而不修改它。
  2. 关注点分离:领域类保持纯粹(只建模业务),序列化策略集中在 mixin 里。

注意「绑定」不等于「多套配置并行」:mixInCache是「每个目标类一条记录」的全局 map,
后注册会覆盖前者(见 3.3 的全局性警示),做不到「A 模块 camelCase、B 模块 snake_case」同时并存。
要按环境隔离,需自建独立的JSONFactory/ObjectReaderProvider,已超出本文的JSON.mixIn范畴。

和「字段级@JSONField(deserializeUsing=...)手写 reader」的区别
手写 reader 是字段级指定一个完整的ObjectReader,强耦合在宿主类的字段上;
mixin 是类级注入注解,由 fastjson2 原生路径执行,宿主类无感。两者可互补:
复杂场景用 reader 精细控制,常规定制用 mixin 声明式解决。

6. 注意事项与最佳实践

  1. 全局状态JSON.mixIn影响整个 JVM。生产环境建议在应用启动阶段集中注册
    规避「目标类作为外层 reader 内嵌字段」这类时序细节(见 3.2)。
  2. 注册时机mixIn会自动作废目标类已缓存的 reader,故对目标类本身无严格先后要求;
    但仍建议集中在启动阶段注册,原因见 3.2 的边界情况。
  3. getter/setter 对齐:字段改名等配置,mixin 靠方法签名投射,序列化管 getter、反序列化管 setter,两边都要标。
  4. 测试清理:测试中注册后用cleanupMixIn()还原,避免污染其他用例。
  5. mixin 类本身不参与序列化:它是纯注解载体,字段/方法体无关紧要(方法签名要和目标类对应,
    fastjson2 靠签名匹配把注解「投射」到目标类的对应成员上)。
  6. 可替代性:能直接在目标类加注解时,优先直接加;mixin 是「改不了才用」的手段,
    不要为了用 mixin 而 mixin(直接注解更直观、无全局状态)。

(END)

http://www.cnnetsun.cn/news/3279554.html

相关文章:

  • 企业级AI编程助手数据安全与合规治理实践指南
  • AI探索地震预测新方法
  • Pikachu靶场XSS通关完整技巧流程
  • MAX30102 PPG信号处理:3种滤波算法对比与STM32F103实现
  • yocto-meta-rockchip常见问题解决:10个Rockchip Yocto构建难题与解决方案
  • 博物馆、名人纪念馆、科技企业展厅设计施工一站式解决
  • 数据结构:设计循环队列
  • YY0505-2012 医用无影灯辐射超标:从 30MHz 到 1GHz 的 6 项滤波整改方案
  • NumPy 2.0 二维数组索引与切片:5种高级用法与性能对比
  • CSAPP BombLab 逆向实战:6个Phase汇编代码逐行解析与GDB调试技巧
  • 基于 Playwright UI 自动化测试
  • ONNX模型C++高性能部署实战:从原理到10倍推理加速
  • PIC18F8722与DTH-08信号切换与上拉下拉配置详解
  • ADS131M02与MK64FX512VDC12的高精度工业测量方案
  • 如何快速掌握中兴光猫配置解密工具:面向开发者的完整指南
  • 微信公众号开发教程
  • Zynq PS 时钟子系统 3 种 PLL 功耗对比:Bypass 模式与动态关断策略
  • Unity AI寻路进阶:NavMeshModifier动态导航网格控制详解
  • AI智能体性能优化:内存管理与模型维护的工程实践
  • 企业微信机器人开发指南:文本消息发送功能实现
  • P8772 [蓝桥杯 2022 省 A] 求和题解前缀和思路
  • 插件化开发:Android端的插件化与ArkUI-X的结合(131)
  • MOSFET GS/G级电阻选型实战:3种应用场景下10K与100Ω阻值对比分析
  • OpenClaw本地AI调度中枢:Node.js版本适配与API路由实战指南
  • Unity URP全屏模糊插件:从Shader原理到工程实践
  • Coze扣子AI智能体开发实战:从零构建多Agent协同应用
  • QP/C++ 7.3.4构建失败:.pack文件缺失的深度解析与解决方案
  • mtr命令,看完这篇就够用!
  • FPGA程序固化实战:Quartus 23.1 转换JIC文件5步避坑指南(含Flash选型)
  • Text2SQL+LangGraph智能体