什么是 MyBatis 插件?

MyBatis 插件是 MyBatis 框架提供的一个扩展机制,它允许开发者在 MyBatis 的执行过程中插入自定义的逻辑。

MyBatis 插件的实现原理

MyBatis 插件的实现原理是通过拦截 MyBatis 的 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler 等组件中的方法来实现的。开发者可以通过实现 MyBatis 提供的 Interceptor 接口,并重写其中的 intercept() 方法,在该方法中实现自定义的逻辑,然后将插件注册到 MyBatis 的 Configuration 中即可。

如何开发 MyBatis 插件?

MyBatis 插件的开发流程如下:

  1. 实现 Interceptor 接口,重写其中的 intercept() 方法,并在该方法中实现自定义的逻辑。
  2. 在 MyBatis 的配置文件中,通过 标签将插件注册到 MyBatis 中。
  3. 配置插件的属性,例如是否启用插件、插件的顺序等。

MyBatis 插件的应用场景是什么?

在实际应用中,MyBatis 插件可以帮助开发者实现许多自定义的功能。例如,通过拦截 Executor 中的 update() 方法,可以实现记录 SQL 执行时间、加解密数据等功能;通过拦截 ResultSetHandler 中的 handleResultSets() 方法,可以实现对查询结果进行拦截和修改,例如分页功能等。同时,MyBatis 插件也可以提高 MyBatis 的性能和扩展性,例如通过缓存查询结果等方式来减少数据库的访问次数,提高系统的性能。

插件开发案例

以下四个例子包括了拦截Executor、StatementHandler、ParameterHandler 和 ResultSetHandler这四个组件

打印sql执行时间插件

如果想要在执行 SQL 语句之前或之后做一些操作,比如修改 SQL 语句,添加日志,统计性能等,可以拦截 Executor 的 update 或 query 方法

例如我要打印sql语句的执行时间:

package cc.oolo.imgcdn.Plugin;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlTimeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取SQL语句
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String sql = mappedStatement.getBoundSql(invocation.getArgs()[1]).getSql();
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        // 执行原始方法
        Object result = invocation.proceed();
        // 记录结束时间
        long endTime = System.currentTimeMillis();
        // 计算并打印执行时间
        long time = endTime - startTime;
        System.out.println("SQL: " + sql + ", Time: " + time + " ms");
        return result;
    }
}

然后再mybatis-config.xml中添加

    <plugins>
        <plugin interceptor="cc.oolo.imgcdn.Plugin.SqlTimeInterceptor" />
    </plugins>

分页插件

如果想要在 SQL 语句生成之前或之后做一些操作,比如修改 SQL 语句,添加分页,分表等,可以拦截 StatementHandler 的 prepare 或 parameterize 方法

以分页为例子:

流程:如果是id为ByPage结尾的方法,则获取方法参数中的PageUtil的page和limit字段进行分页,并且将总数赋值给PageUtil的count字段,然后对原sql进行修改,添加limit字段和参数。

定义一个PageUtil类:

package cc.oolo.imgcdn.Plugin.Pojo;

import cc.oolo.imgcdn.Plugin.Anno.Encrypted;

public class PageUtil {
    private int pageNum;
    private int pageSize;
    private int count;

    public PageUtil() {
    }

    public PageUtil(int pageNum, int pageSize) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}
 

创建 PaginationInterceptor

package cc.oolo.imgcdn.Plugin;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;

import cc.oolo.imgcdn.Plugin.Pojo.PageUtil;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;


@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
public class PaginationInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 从调用目标中获取语句处理器
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // MetaObject是一个用于访问对象属性的工具类,它基于Java的反射机制实现。
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
        // 获取映射语句的id
        String value = (String) metaObject.getValue("delegate.mappedStatement.id");
        // 如果id以ByPage结尾,说明是一个分页查询
        if (value.endsWith("ByPage")) {
            // 从调用参数中获取连接
            Connection connection = (Connection) invocation.getArgs()[0];
            // 获取原始sql
            String sql = statementHandler.getBoundSql().getSql();
            // 在执行原始sql之前,执行一个count语句来获取记录的总数
            String count = "select count(0) from ("+ sql +") a";
            // 使用count sql创建一个预处理语句
            PreparedStatement preparedStatement = connection.prepareStatement(count);
            // 获取参数处理器
            ParameterHandler paramHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            // 使用参数处理器为预处理语句设置参数
            paramHandler.setParameters(preparedStatement);
            // 执行count查询并获取结果集
            ResultSet resultSet = preparedStatement.executeQuery();
            // 如果mapper方法有多个参数,paramHandler.getParameterObject()是一个Map
            if (paramHandler.getParameterObject() instanceof Map){
                Map<String, Object> o = (Map<String,Object>)paramHandler.getParameterObject();
                PageUtil pageUtil = null;
                // 遍历参数,找到PageUtil对象(如果存在)
                for (Object v : o.values()) {
                    if (v instanceof PageUtil){
                        pageUtil = (PageUtil) v;
                        // 如果有PageUtil对象,从结果集中获取count值并设置到PageUtil对象中
                        if (resultSet.next()) {
                            pageUtil.setCount(resultSet.getInt(1));
                        }
                    }
                }
                // 在原始sql后面添加一个limit子句来进行分页
                String pageSql = sql + " limit " + (pageUtil.getPageNum()-1) * pageUtil.getPageSize() + ", " + pageUtil.getPageNum() * pageUtil.getPageSize();
                System.out.println("处理前sql:" + sql);
                System.out.println("处理后sql:" + pageSql);
                // 将修改后的sql覆盖原sql语句
                metaObject.setValue("delegate.boundSql.sql",pageSql);
            }else{
                //如果mapper方法只有一个参数,paramHandler.getParameterObject()就是该对象
                PageUtil pageUtil = (PageUtil) paramHandler.getParameterObject();
                pageUtil.setCount(resultSet.getInt(1));
                // 在原始sql后面添加一个limit子句来进行分页
                String pageSql = sql + " limit " + (pageUtil.getPageNum()-1) * pageUtil.getPageSize() + ", " + pageUtil.getPageNum() * pageUtil.getPageSize();
                // 将修改后的sql覆盖原sql语句
                metaObject.setValue("delegate.boundSql.sql",pageSql);
            }
            // 关闭结果集和预处理语句
            resultSet.close();
            preparedStatement.close();
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
       return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

}

在使用时,只需要在mapper的抽象方法添加PageUtil参数,然后创建PageUtil对象,给pageNum和pageSize赋值,然后将pageUtil对象作为参数即可:

返回结果:

{
    "success": true,
    "code": 200,
    "message": "成功",
    "data": {
        "agent": [
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "123456"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "123456"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "123456"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "张三",
                "age": 1,
                "address": "重庆市",
                "phone": "1008611",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            }
        ],
        "page": {
            "pageNum": 1,
            "pageSize": 10,
            "count": 13
        }
    }
}

加密插件

如果想要在 SQL 语句的参数设置之前或之后做一些操作,比如修改参数值,添加类型转换等,可以拦截 ParameterHandler 的 setParameters 方法

我们以加密的场景为例,定义一个注解 @Encrypted,参数为type,表示使用的加密方式

package cc.oolo.imgcdn.Plugin.Anno;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
//表示注解是标注在字段上面
@Target({ElementType.FIELD})
public @interface Encrypted {
    String type() default "md5";
}

因为需要根据注解的值选择使用不同的加密方法,这里采用了 策略模式+工厂模式

定义一个 EncryptionStrategy 接口

package cc.oolo.imgcdn.Plugin.Encrypt;

public interface EncryptionStrategy {
    String encrypt(String text);
}

创建 Md5EncryptionStrategy 类,实现 EncryptionStrategy 接口

package cc.oolo.imgcdn.Plugin.Encrypt;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5EncryptionStrategy implements EncryptionStrategy{
    @Override
    public String encrypt(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(text.getBytes());
            byte[] digest = md.digest();
            StringBuffer sb = new StringBuffer();
            for (byte b : digest) {
                sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
}

创建一个工厂类 EncryptionFactory,使用map,将对应的实现类注册到工厂中

package cc.oolo.imgcdn.Plugin.Encrypt;

import java.util.HashMap;
import java.util.Map;

public class EncryptionFactory {

    public static Map<String,EncryptionStrategy> map = new HashMap<>();

    static {
        map.put("md5",new Md5EncryptionStrategy());

    }

    public static String encode(String type,String text){
       return map.get(type).encrypt(text);
    }

}

编写插件:

package cc.oolo.imgcdn.Plugin;


import cc.oolo.imgcdn.Plugin.Anno.Encrypted;
import cc.oolo.imgcdn.Plugin.Encrypt.EncryptionFactory;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Map;

@Intercepts(
        @Signature(type = ParameterHandler.class,method = "setParameters",args = {PreparedStatement.class})
)
public class EncryptedPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler paramHandler = (ParameterHandler) invocation.getTarget();
        // 如果 mapper 中抽象方法的参数有多个,那么paramHandler.getParameterObject()拿到的对象是一个Map,如果只有一个,那么拿到的对象就是该对象,所以这里需要进行判断
        if (paramHandler.getParameterObject() instanceof Map){
            Map<String, Object> map = (Map<String, Object>) paramHandler.getParameterObject();
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                // 如果有多个参数,那么每个参数都会对应有一个param,其value跟原有参数是相同的,所以会重复处理
                // 跳过以"param"开头的键
                if (key.startsWith("param")){
                    continue;
                }
                // 根据加密注解设置值对象的字段
                setFields(value);
            }
        }else{
            // 如果参数对象不是一个map,直接获取它
            Object parameterObject = paramHandler.getParameterObject();
            setFields(parameterObject);
        }
        // 继续执行调用
        return invocation.proceed();
    }

    /**
     * 根据加密注解设置参数对象的字段
     * @param parameterObject
     * @throws IllegalAccessException
     */
    private void setFields(Object parameterObject) throws IllegalAccessException {
        for (Field field : parameterObject.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Encrypted.class)){
                // 如果字段有加密注解,获取它的类型
                Encrypted encrypted = field.getAnnotation(Encrypted.class);
                String type = encrypted.type();
                field.setAccessible(true);
                // type 为@Encrypted注解的值,通过工厂类选择不同的加密方式
                String encode = EncryptionFactory.encode(type, String.valueOf(field.get(parameterObject)));
                // 将字段值设置为编码后的值
                field.set(parameterObject,encode);
            }
        }
    }
}

在实体类的字段上添加 @Encrypeted(type = "md5") 即可对该字段进行加密

package cc.oolo.imgcdn.Entity;

import cc.oolo.imgcdn.Plugin.Anno.Encrypted;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AgentEntity {
//    private String id;
    private String date;
    private String name;
    private int age;
    private String address;
    private String phone;
    private String email;
    @Encrypted(type = "md5")
    private String password;
}

测试:使用postman插入一条数据

http://localhost:8090/insertAgent
ResultBody(json);
{
    "date":"2023-01-01",
    "name":"张三",
    "age": 1,
    "address": "重庆市",
    "phone": "1008611",
    "email":"812257301@qq.com",
    "password":"123456"
}

数据脱敏插件

如果想要在 SQL 语句的结果集处理之前或之后做一些操作,比如修改结果集,添加数据过滤等,可以拦截 ResultSetHandler 的 handleResultSets 或 handleResult 方法

以数据脱敏为例:有时候我们不希望将用户完整的姓名或者手机号展示给前端,通常都会在手机号或者名字部分字符以*代替,例如 133****4654,张*,或 张*三

定义一个接口,并且继承 Function<String, Object> ,表示该接口是一个函数式接口

package cc.oolo.imgcdn.Plugin.Desensitization;

import java.util.function.Function;

public interface SensitiveInterface extends Function<String, Object> {
}

创建一个枚举类,每一个枚举实例使用了lambda表达式,也就是说每个枚举实例都可以看作是一个匿名内部类

package cc.oolo.imgcdn.Plugin.Desensitization;

public enum SensitiveEnum {
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)","$1*$2")),
    ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})","$1****$2")),
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})","$1****$2")),
    ADDRESS(s -> s.replaceAll("(\\S{8})\\S{4}(\\S*)\\S{4}","$1****$2****")),
    ;

    private final SensitiveInterface sensitiveInterface;

    SensitiveEnum(SensitiveInterface sensitiveInterface) {
        this.sensitiveInterface = sensitiveInterface;
    }

    public SensitiveInterface getSensitiveInterface() {
        return sensitiveInterface;
    }
}

自定义注解 @Sensitive,该注解应用在字段上

package cc.oolo.imgcdn.Plugin.Desensitization;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
    SensitiveEnum strategy();
}

创建 mybatis 拦截器类,先执行,获取结果,然后通过反射遍历结果,判断字段是否存在 @Sensitive 注解,如果存在,则获取注解中指定的脱敏策略,并使用该策略对该字段的值进行脱敏处理。

package cc.oolo.imgcdn.Plugin;

import cc.oolo.imgcdn.Plugin.Desensitization.Sensitive;
import cc.oolo.imgcdn.Plugin.Desensitization.SensitiveEnum;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;

@Intercepts(
        @Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class})
)
public class DesensitizationPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        List<Object> proceed = (List<Object>)invocation.proceed();
        for (Object o : proceed) {
            masking(o);
        }
        return proceed;
    }

    private void masking(Object source) throws IllegalAccessException {
        Class<?> aClass = source.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            if (declaredField.isAnnotationPresent(Sensitive.class)) {
                Sensitive annotation = declaredField.getAnnotation(Sensitive.class);
                SensitiveEnum strategy = annotation.strategy();
                declaredField.setAccessible(true);
                Object apply = strategy.getSensitiveInterface().apply(String.valueOf(declaredField.get(source)));
                declaredField.set(source,apply);
            }
        }
    }
}

测试:

package cc.oolo.imgcdn.Entity;

import cc.oolo.imgcdn.Plugin.Anno.Encrypted;
import cc.oolo.imgcdn.Plugin.Desensitization.Sensitive;
import cc.oolo.imgcdn.Plugin.Desensitization.SensitiveEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AgentEntity {
//    private String id;
    private String date;
    @Sensitive(strategy = SensitiveEnum.USERNAME)
    private String name;
    private int age;
    private String address;
    @Sensitive(strategy = SensitiveEnum.PHONE)
    private String phone;
    private String email;
    @Encrypted(type = "md5")
    private String password;
}

结果:

{
    "success": true,
    "code": 200,
    "message": "成功",
    "data": {
        "agent": [
            {
                "date": "2023-01-01",
                "name": "张*",
                "age": 1,
                "address": "重庆市",
                "phone": "133****9874",
                "email": "812257301@qq.com",
                "password": "e10adc3949ba59abbe56e057f20f883e"
            },
            {
                "date": "2023-01-01",
                "name": "王*",
                "age": 1,
                "address": "重庆市",
                "phone": "183****9876",
                "email": "812257301@qq.com",
                "password": "fcea920f7412b5da7be0cf42b8c93759"
            },
            {
                "date": "2023-01-01",
                "name": "王*五",
                "age": 1,
                "address": "重庆市",
                "phone": "143****9876",
                "email": "812257301@qq.com",
                "password": "95d47be0d380a7cd3bb5bbe78e8bed49"
            },
            {
                "date": "2023-01-01",
                "name": "王*六",
                "age": 1,
                "address": "重庆市",
                "phone": "143****9526",
                "email": "812257301@qq.com",
                "password": "b3c690dd568e6d8b5f82053d47356581"
            }
        ],
        "page": {
            "pageNum": 1,
            "pageSize": 10,
            "count": 4
        }
    }
}
最后修改:2023 年 07 月 06 日
如果觉得我的文章对你有用,请随意赞赏