前言
又是好长时间没有写分享了,对我来说,写博客消耗的心力太大了,只能偶尔心血来潮了,没法收回曾经立下的Flag了。
一、背景
最近有数据脱敏的需求,我三下五除二就解决了,就是太丑了,直接耦合在业务处理中。所以我一直在寻找更加优雅的方案,例如 Mybatis 插件了,工具类等等,但都感觉差点意思。终于 当我看到了这篇博文:自定义注解实现数据序列化时进行数据脱敏,这就是我想要的,使用 Jackson 注解。但当我看完后马上意识到,他的方式还不够灵活。
我 立刻就有了改造的灵感,下文就是 使用 外观 + 策略 + 命令 + 工厂 + 静态代理 + 装饰器 + 解释器 + 适配器 + 享元 + 桥接 + 模板(有点牵强了,就是为了硬凑贯口) 改造后的成品,提供更强的灵活性。
通过这个样例,可以学习三个事情
- 学习Jackson 这种提供扩展的方式,太方便了
- 设计模式只是参考,六大原则才是核心目标,没有必要太教条。
- lambda 相关技术太方便了, 尤其对设计模式的实现,如果可以尽量多用。
二、代码实现
2.1 对脱敏操作抽象出函数式接口
这个接口 是 策略 + 命令 结合后的抽象,并通过 default
函数实现装饰功能。使用注解Sensitive
实例来实现了命令模式 ,可以更灵活的控制脱敏执行。
@FunctionalInterface
public interface SensitiveDeal {
String apply(String source, Sensitive sensitive);
default SensitiveDeal compose(SensitiveDeal before) {
Objects.requireNonNull(before);
return (source, sensitive) -> apply(before.apply(source, sensitive), sensitive);
}
default SensitiveDeal andThen(SensitiveDeal after) {
Objects.requireNonNull(after);
return (source, sensitive) -> after.apply(apply(source, sensitive), sensitive);
}
}
2.2 Jackson自定义注解
@Documented 表示该注解会生成到 javaDoc中
@Retention(RetentionPolicy.RUNTIME) 表示该注解在运行时生效
@Target(ElementType.FIELD) 表示注解作用于字段上
@JacksonAnnotationsInside 这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) 指定使用自定义的序列化器
参数说明见源码注释。
Sensitive
可以使用相同的一套参数来配置脱敏实现,可以说 当前的脱敏方案 提供了 统一的外观。
Strategy.CUSTOMER
策略提供了适配的支持,使用Sensitive.dealType
就可以将第三方实现嵌入当前框架。
重点注意一下 Strategy
继承了 SensitiveDeal
,实现了静态代理,体会这种写法的便利。
Strategy
的实例化使用了 简单工厂,策略 + 命令模式,稍微有点复杂,但是对外暴露的时候,就是普通的策略,使用者并不关心策略本身是如何被创建的。
Sensitive.regular
正则表达式 使用了 解释器模式,这个使用有点过于划水了。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
/**
* 脱敏策略,参考{@link Sensitive.Strategy}
*/
Strategy value();
/**
* 有无参构造函数的{@link SensitiveDeal}实现类,用于完全自定义脱敏,参考
* {@link Sensitive.Strategy#CUSTOMER}
*/
Class<? extends SensitiveDeal> dealType() default SensitiveDeal.class;
/**
* 正则表达式,参考具体策略的注释。
*/
String regular() default "";
/**
* 替换后显示,与 {@link Sensitive#regular()}配合可以实现更方便的正则替换。<br>
* 剩余情况默认只取首位字符来填充。
*/
String replacement() default "*";
/**
* 左侧需要保留几位明文字段,参考具体策略的注释。
*/
int maskStart() default 0;
/**
* 右侧最多保留几位明文,参考具体策略的注释。
*/
int keepTail() default 0;
/**
* 具体的脱敏策略实现
*
* @author zkr-liuchunming
* @see SensitiveDeals
* @see SensitiveDeal
*
*/
enum Strategy implements SensitiveDeal {
// 以下是 与业务字段相关的策略
/**
* 性别
*/
SEX(SensitiveDeals::none),
/**
* 用户名
*/
USERNAME(SensitiveDeals::userName),
/**
* 密码
*/
PASSWORD(SensitiveDeals::none),
/**
* 证件号码
*/
IDNO(SensitiveDeals.createWrap(6, 4)),
/**
* 座机号
*/
FIXED_PHONE(SensitiveDeals.createSuffix(4)),
/**
* 手机号
*/
MOBILE_PHONE(SensitiveDeals.createWrap(3, 4)),
/**
* 邮箱
*/
EMAIL(SensitiveDeals::email),
/**
* 银行账号
*/
BANK_ACCNO(SensitiveDeals.createWrap(6, 4)),
/**
* 地址
*/
ADDRESS(SensitiveDeals.createRegular("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****")),
// 以下是 与业务无关的通用策略,如果经常脱敏某种业务字段,建议抽成 业务字段策略,更有可读性
/**
* 全部隐藏,例如密码之类
*/
NONE(SensitiveDeals::none),
/**
* 只留结尾四个字符
*/
SUFFIX_4(SensitiveDeals.createSuffix(4)),
/**
* 只保留结尾的通用脱敏,使用 {@link Sensitive#keepTail()}
*/