应使用contextualserializer配合自定义注解实现字段级脱敏,而非stdserializer;因后者无法感知嵌套结构、类型信息与上下文,易导致类型擦除、空指针及策略失效。

StdSerializer 本身不适合处理复杂 JSON 结构的脱敏与加密逻辑——它只面向单个字段值,无法感知嵌套对象、数组或字段间上下文关系。 真正该用的是 ContextualSerializer 配合自定义注解,否则你会在序列化深层字段时反复丢失类型信息、触发空指针、或把整个对象当成字符串粗暴脱敏。
为什么 StdSerializer 在复杂结构里会失效
当你写一个继承 StdSerializer<object></object> 的类,并试图在 serialize() 里做“如果字段名是 phone 就脱敏、如果是 idCard 就走另一套规则”,这看似可行,实则踩了三个坑:
-
Object类型擦除后,你拿不到原始字段类型(比如是User还是List<order></order>),value.getClass()返回的常是LinkedHashMap或String,根本没法做业务判断 - 嵌套对象中的
phone字段不会自动触发你的StdSerializer,除非你显式给每一层都加@JsonSerialize(using = ...),这等于放弃“自动”二字 - 你无法从
SerializerProvider中安全获取 Spring 上下文(比如当前用户角色、租户 ID),而这些恰恰是动态脱敏策略(如“管理员可见全量,普通用户仅见前3后4”)的必要输入
ContextualSerializer 才是正确入口点
真正能拿到字段元数据(包括注解、声明类型、泛型参数)的地方,是 ContextualSerializer 的 createContextual() 方法。它会在每个字段被序列化前调用一次,传入 BeanProperty —— 这才是你做条件分支的唯一可靠依据。
典型做法是:
- 定义一个注解,比如
@Desensitize(strategy = PhoneStrategy.class, when = "user_role == 'GUEST'") - 在
createContextual()中通过property.getAnnotation(Desensitize.class)拿到注解实例 - 检查
property.getType().getRawClass()是否为String.class,避免对Integer或LocalDateTime错误应用脱敏 - 根据注解参数 new 出对应策略实例(如
new PhoneStrategy(3, 4, "*")),并返回一个轻量级的JsonSerializer<string></string>匿名子类
注意:createContextual() 内不要 new 大对象、不要查 DB、不要调远程服务——它在每次字段序列化时都执行,性能敏感。
DMap GL地图JSAPI(dmapgl)开发指南。用于编写、审查、调试 DMap 地图 API 代码,涵盖地图初始化、覆盖物渲染、图层管理、事件处理、控件交互等任务。当用户提及 dmap、dmapgl、jsapi‑gl 或相关地图开发需求时自动触发。
加密逻辑必须和序列化解耦
如果你真需要加密(比如 AES 加密手机号再 base64),别把它塞进 serialize()。原因很实际:
- 加密密钥管理、加解密异常、IV 生成等逻辑和序列化职责无关,混在一起会让调试变困难
- 加密结果通常是
byte[]或String,但 Jackson 默认会把byte[]序列为 base64 字符串——你若手动gen.writeString(encrypted),反而可能多套一层 base64 - 更关键的是:加密应发生在业务层或 DTO 构建阶段,而不是序列化临界点。否则同一个对象被多次序列化(如日志打印、缓存写入、HTTP 响应)就会重复加密,导致解密失败
推荐做法:在 Controller 返回前,用 AOP 或 DTO 构造器统一调用 EncryptionService.encrypt(phone),然后让 Jackson 按原样序列化加密后的字符串。脱敏和加密是两类动作,不该共用一个扩展点。
嵌套结构脱敏的唯一可靠方式是字段级控制
面对类似 Order → customer: { name, phone, address } → address: { street, city, zip } 这样的结构,你不能指望一个全局 StdSerializer 自动递归识别所有 phone 字段。Jackson 不会为你做字段名匹配遍历。
可行路径只有两条:
- 在每个需脱敏的字段上明确标注注解,如
@Desensitize(type = DesensitizeType.PHONE),靠ContextualSerializer逐个处理 - 用
@JsonSerialize(using = CustomerViewSerializer.class)直接接管整个Customer对象序列化,在其serialize()里手动调用jsonGen.writeObjectField("phone", masker.mask(customer.getPhone()))等逻辑——但这丧失了复用性,且容易漏掉新增字段
前者是主流选择;后者只适合极少数强管控场景。别幻想“写一个 Serializer 解决所有嵌套脱敏”——那不是 Jackson 的设计哲学,而是给自己埋雷。










