jackson ContextualSerializer 详解

Last Modified: 2023/09/27

概述

本文将会介绍如何利用 ContextualSerializer 实现数据脱敏,但脱敏不是本文的关注点,我们专注的是怎么优雅的实现数据脱敏类似的需求。

本文目标

我们知道在返回数据到前端的时候,手机号等敏感信息,需要脱敏。用人话讲就是将部分内容用 * 号代替,假设有个 User 模型定义如下

// 假设一个人有两个手机号
public class User {
    private String mobile;
    private String mobile2;
    // 省略 getter/setter
}

我们希望返回前端的 json 数据是经过脱敏之后的数据,示例如下:

{
  "mobile": "****6523946", 
  "mobile2": "****6523947"
}

实现方法

为了实现这个目标,我们可以实现一个简单的替换函数处理下 mobile 和 mobile2,将部分内容替换成 * 号即可。但这不符合本文目标,毕竟我们要的是优雅而不是简单。。。

为了实现装逼的目标,我们可以自定义一个 JsonSerializer,假设该 Serializer 的名字为 SimpleMaskMobileSerializer,并将其应用到 User 类上。

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

public class User {
  @JsonSerialize(using = SimpleMaskMobileSerializer.class)
  private String mobile;
  @JsonSerialize(using = SimpleMaskMobileSerializer.class)
  private String mobile2;
}

SimpleMaskMobileSerializer 实现如下

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.util.StringUtils;

import java.io.IOException;

public class SimpleMaskMobileSerializer extends JsonSerializer<String> {
  @Override
  public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    if (!StringUtils.hasText(s)) {
      jsonGenerator.writeString(s);
      return;
    }
    jsonGenerator.writeString("****" + s.substring(4));
  }
}

这已经够优雅了吗?no,还可以更进一步,@JsonSerialize(using = SimpleMaskMobileSerializer.class) 这种写法太啰嗦了,可以不可以直接用我们自定义的注解呢?可以! 接下来我们实现一个 @MobileMask 注解,只需要将该注解标注到 mobile 字段即可,注解实现如下:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SimpleMaskMobileSerializer.class)
public @interface MobileMask {
}

这里需要注意的是必须使用 @JacksonAnnotationsInside 元注解标注 @MobileMask。

到这里似乎跟我们的标题完全不想干,没关系啊,接下来就相关了。新的需求是这样的:要求 @MobileMask 能够接收一个参数,该参数用于指定脱敏的数据包含多少个 * 号。注解用法如下:

public class User {
 @MobileMask(masks = 2)
 private String mobile;
 @MobileMask(masks = 4)
 private String mobile2;
}

此时返回前端的样例数据如下所示,一个包含两个 * 号,一个包含 4 个 * 号。

{
  "mobile": "**656523946", 
  "mobile2": "****6523947"
}

使用 ContextualSerializer 实现需求

先修改注解

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = MobileMaskSerializer.class)
public @interface MobileMask {
  int masks();
}

这里的 MobileMaskSerializer 就是重点,它需要实现 ContextualSerializer,为了突出重点,省略了 import 语句和一些边界判断,完整代码可以移步 Github 查看。

public class MobileMaskSerializer extends JsonSerializer<String> implements ContextualSerializer {

  private int maskCount;
  
  @Override
  public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < maskCount; i++) {
          sb.append("*");
      }
      jsonGenerator.writeString(sb.toString() + s.substring(maskCount));
  }
  
  @Override
  public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
      MobileMask mobileMask = beanProperty.getAnnotation(MobileMask.class);
      MobileMaskSerializer ret = new MobileMaskSerializer();
      ret.setMaskCount(mobileMask.masks());
      return ret;
  }
  
  public void setMaskCount(int maskCount) {
      this.maskCount = maskCount;
  }
}

实现的重点在于 createContextual 方法,该方法需要返回一个 JsonSerializer 对象,通过 beanProperty 可以获取当前属性或者方法上标注的注解,进而拿到注解的 masks 属性。有了这个属性的值,我们就知道在序列化 mobile 的时候究竟需要屏蔽多少个字符。

createContextual 实现要点:

  • 不要修改 this 然后返回 this,虽然 this 显然也是 createContextual 对象,如果 this 不适用,应该返回一个新的对象,就像上面的实现一样返回一个 new MobileMaskSerializer()
  • 如果 this 适用,那么可以直接返回 this;
  • 如果实在不确定是否适用,那就返回一个新对象;

到底怎么定义适用还是不适用?

可以想象一下:@MobileMask(masks=xx) 可以被应用到很多类的很多字段,但是大多数传递的 masks 的数量都是一样的。那么在这些 masks 数量一致的地方可以共用一个 MobileMask 实例。那怎么实现呢?感兴趣的可以参考:MobileMaskSerializer2.java

有问题吗?点此反馈!

温馨提示:反馈需要登录