jackson ContextualSerializer 详解
概述
本文将会介绍如何利用 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
温馨提示:反馈需要登录