废话不多少,终于弄懂了mybatis plugin

一、前言

1月份已经过了一半多,天气回暖了许多,今天就来学习一下mybatis插件相关内容,可能mybatis插件使用得很少,但是对于某一些业务场景,plugin可以帮助解决一些问题,就比如脱敏问题,我们在实际中,我们需要导出Excel,但是并不希望用户信息完整的展示出来,所以我们可以脱敏,姓名只显示杨楠、 151*1234等等,所以plugin可以结合相应的业务场景进行开发

二、mybatis plugin介绍

2.1 四大核心对象

  • ParameterHandler:用来处理传入SQL的参数,我们可以重写参数的处理规则。
getParameterObject, setParameters
  • ResultSetHandler:用于处理结果集,我们可以重写结果集的组装规则。 handleResultSets, handleOutputParameters 复制代码
  • StatementHandler:用来处理SQL的执行过程,我们可以在这里重写SQL非常常用。
prepare, parameterize, batch, update, query
  • Executor:是SQL执行器,包含了组装参数,组装结果集到返回值以及执行SQL的过程,粒度比较粗。
update, query, flushStatements, commit, rollback,getTransaction, close, isClosed

2.2 Interceptor

想要开发mybatis插件,我们只需要实现org.apache.ibatis.plugin.Interceptor接口,以下为Interceptor接口的结构:

public interface Interceptor {

  //代理对象每次调用的方法,就是要进行拦截的时候要执行的方法。在这个方法里面做我们自定义的逻辑处理
  Object intercept(Invocation invocation) throws Throwable;

  /**
   *生成target的代理对象,通过调用Plugin.wrap(target,this)来生成
   */
  Object plugin(Object target);

  //用于在Mybatis配置文件中指定一些属性的,注册当前拦截器的时候可以设置一些属性
  void setProperties(Properties properties);

}

昨天我们刚刚学习了JDK代理,下面我们一起来看看Invocation对象

public class Invocation {

  //需要拦截的对象
  private final Object target;
  //拦截target中的具体方法,也就是说Mybatis插件的粒度是精确到方法级别的。
  private final Method method;
  //拦截到的参数。
  private final Object[] args;
  ……getter/setter……

   //proceed 执行被拦截到的方法,你可以在执行的前后做一些事情。
   public Object proceed() throws InvocationTargetException, IllegalAccessException {
            //执行目标类的目标方法
            return method.invoke(target, args);
  }

}

以上的proceed方法(method.invoke(target, args))是否似曾相识,因为昨天在写代理的时候,JDK代理用到了该方法,执行被代理类的方法,表示指定目标类的目标方法

2.3 拦截签名

因为我们在Interceptor中提到了Invocation,主要用于获取拦截对象的信息,并且执行相应的方法,但是我们应该如何获取Invocation对象?

@Intercepts(@Signature(type = ResultSetHandler.class, //拦截返回值
        method = "handleResultSets", //拦截的方法
        args = {Statement.class}))//参数

这样其实就创建了一个Invocation对象

@Signature参数

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {

  //定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
  Class<?> type();

  //在定义拦截类的基础之上,在定义拦截的方法,主要是看type里面取哪一个拦截的类,取该类当中的方法
  String method();

  //在定义拦截方法的基础之上在定义拦截的方法对应的参数,
  Class<?>[] args();
}

举个例子:

比如,我们需要拦截Executor类的update方法,思路如下:

  • 定义Interceptor的实现类
public class MybatisPlugin implements Interceptor{}
  • 增加@Intercepts注解,标识需要拦截的类、方法、方法参数
/**
 * 插件签名,告诉当前的插件用来拦截哪个对象的哪种方法
 */
@Intercepts(
        @Signature(
                type = Executor.class, 
                method = "update",
                args = {MappedStatement.class,Object.class}
                )
)
@Slf4j
public class MybatisPlugin implements Interceptor {}

@Signature注解:
  1.type:  表示所需要拦截的类为Executor
  2.method:为Executor类中的update方法
  3.args:  这个对应update的的参数,如下:
                    int update(MappedStatement ms, Object parameter)
  • 实现intercept、plugin、setProperties方法
/**
 * 拦截目标对象的目标方法
 * @param invocation
 * @return
 * @throws Throwable
 */
@Override
public Object intercept(Invocation invocation) throws Throwable {
    log.info("拦截目标对象:{}",invocation.getTarget());
    Object proceed = invocation.proceed();
    log.info("intercept拦截到的返回值:{}",proceed);
    return proceed;
}

/**
 * 包装目标对象 为目标对象创建代理对象
 * @param target
 * @return
 */
@Override
public Object plugin(Object target) {
    log.info("将要包装的目标对象:{}",target);
    //创建代理对象
    return Plugin.wrap(target,this);
}

/**
 * 获取配置文件的属性
 * @param properties
 */
@Override
public void setProperties(Properties properties) {
    log.info("获取配置文件参数");
}
  • 注入plugin
@Configuration
public class MybatisPluginBean {

    @Bean
    public MybatisPlugin mybaticPlugin(){
        return new MybatisPlugin();
    }
}

2.4 MetaObject

Mybatis提供了一个工具类:

org.apache.ibatis.reflection.MetaObject

它通过反射来读取和修改一些重要对象的属性。我们可以利用它来处理四大对象的一些属性,这是Mybatis插件开发的一个常用工具类。

Object getValue(String name) 根据名称获取对象的属性值,支持OGNL表达式。
void setValue(String name, Object value) 设置某个属性的值。
Class<?> getSetterType(String name) 获取setter方法的入参类型。
Class<?> getGetterType(String name) 获取getter方法的返回值类型。
通常我们使用SystemMetaObject.forObject(Object object)来实例化MetaObject对象。

三、mybatis脱敏插件

  • 1、首先,定义函数接口,用于存储脱敏策略
  • 2、定义注解,用于标识需要脱敏属性
  • 3、实现Interceptor接口,用于处理脱敏操作
  • 4、注册插件

3.1 定义函数接口

JDK8开始,加入了函数式编程接口,之前我们给对象传递的都是值,但是现在我们可以传递表达式,我们只需要继承Function<String,String>

/**
 * 函数式接口
 */
public interface Declassified extends Function<String,String> {

}

3.2 定义脱敏策略

脱敏策略,主要是约定名字应该如何脱敏,将杨羽茉转换为杨*

/**
 * 脱敏的策略
 */
public enum SensitiveStrategy{

    //定义名称脱敏处理表达式
    NAME(s -> s.replaceAll("(\\S)\\S(\\S*)","$1*$2"));

    private Declassified declassified;

    //注入脱敏函数式接口
     SensitiveStrategy(Declassified declassified){
        this.declassified = declassified;
    }

    //获取脱敏函数式接口
    public Declassified getDeclassified() {
        return declassified;
    }
}

写到这里,我们进行拓展,学习s.replaceAll()的使用方式:

replaceAll(): 给定的参数 replacement 替换字符串所有匹配给定的正则表达式的子字符串

public String replaceAll(String regex, String replacement)

regex -- 匹配此字符串的正则表达式。

replacement -- 用来替换每个匹配项的字符串。
复制代码
@Test
    void contextLoads() {
        this.strProcess("杨羽茉");
    }

public void strProcess(String name){
   //意思:将name转换为$1*$2方式,也就是将中间的字符替换为a*b
   String s = name.replaceAll("(\\S)\\S*(\\S)", "$1*$2");
   System.out.println("转换的字符串:"+s);
}

输出结果:
            转换的字符串:杨*茉

再来一个例子,脱敏手机号码:

public void strProcess(String phone){
        //将手机号码转换为131***2172的方式
    //(\\d{3})代表$1显示前3位
    // \\d* 代表中间部分替换为***
    //(\\d{4}) 代表$2显示后三位
        String s = phone.replaceAll("(\\d{3})\\d*(\\d{4})", "$1***$2");
        System.out.println("转换的字符串:"+s);

    }

3.2 定义脱敏注解

/**
 * 脱敏注解:标识需要脱敏的字段,并且指定具体的脱敏策略
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
    //脱敏策略
    SensitiveStrategy strategy();
}

3.4 实现Interceptor接口

/**
 * mybatis插件
 */
@Intercepts(
        @Signature(
                type = ResultSetHandler.class, //表示需要拦截的是返回值
                method = "handleResultSets", //表示需要拦截的方法
                args = {Statement.class} //表示需要拦截方法的参数
        )
)

public class MybatisPlugin implements Interceptor {

    private Logger log = LoggerFactory.getLogger(MybatisPlugin.class);

    /**
     * 拦截方法
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //执行目标方法
        List<Object> records = (List<Object>) invocation.proceed();
        records.forEach(this::sensitive);
        return records;
    }

    /**
     * 过滤@Sensitive注解
     * @param source
     */
    private void sensitive(Object source){
        //获取返回值类型
        Class<?> sourceClass = source.getClass();

        //获取返回值的metaObject:通过反射来读取和修改一些重要对象的属性
        MetaObject metaObject = SystemMetaObject.forObject(source);

        //我们在拦截的时候,需要过滤没有@Sensitive注解的属性,如果有注解,在doSensitive中进行脱敏操作
        Stream.of(sourceClass.getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Sensitive.class))
                .forEach(field -> doSensitive(metaObject,field));
    }

    /**
     * 脱敏操作
     * @param metaObject
     * @param field
     */
    private void doSensitive(MetaObject metaObject, Field field){
        //获取属性名
        String name = field.getName();
        //获取属性值
        Object value = metaObject.getValue(name);

        //只有字符串才可以脱敏
        if(String.class == metaObject.getGetterType(name) && value != null){
            //获取自定义注解
            Sensitive annotation = field.getAnnotation(Sensitive.class);
            //获取自定义注解的参数
            SensitiveStrategy strategy = annotation.strategy();
            //脱敏操作
            String apply = strategy.getDeclassified().apply((String) value);

            //将脱敏后的数据放回返回值中
            metaObject.setValue(name,apply);
        }
    }

    /**
     * 生成代理对象
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    /**
     * 设置属性
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

3.5 注册插件

@Configuration
public class MybatisPluginConfig {

    @Bean
    public MybatisPlugin mybatisPlugin(){
        return new MybatisPlugin();
    }

}

使用注解

@Sensitive(strategy = SensitiveStrategy.NAME)
private String name;

结果

{
  userId=1,
  name=管*员
}

Mybatis plugin已经完成,明天开始要认真的开始系统化的学习netty相关内容,明天写一篇netty整合websocket,晚安!

作者:Yangzinan
链接:https://juejin.cn/post/6919846755995484168
来源:掘金

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章