mybatis 插件机制

mybatis 提供了一种插件功能,虽然叫插件,其实是拦截器功能。底层通过采用责任链设计模式,代理原来的 mybatis 相关对象来改变默认行为,比如修改 sql 等

没有插件的运行图

图片Base64编码

有插件的运行图

图片Base64编码

拦截对象与方法

如上图,允许拦截的对象有 4 个,分别是 Executor(执行器)、ParameterHandler(参数处理)、ResultSetHandler(结果处理)、StatementHandler(sql 构建处理)。允许拦截的方法就是这 4 个对象具有的方法。里面的方法都很清晰,比如 Executor 的 query、update 方法对应查询和更新、 StatementHandler 的 preper 方法对应设置 sql 参数

插件示例

举例打印 sql 和数据权限

插件编写

package com.dtyunxi.yundt.cube.center.item.svr.config;

import com.dtyunxi.yundt.cube.center.item.api.Permission;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.core.annotation.Order;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

/**
 * 1,可以拦截多个对象和方法,@Intercepts 是个数组
 * 2,@Signature - type:拦截对象,method:拦截对象的方法名,args:拦截对象方法的参数
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisInterceptorUpdateSql implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        
        // invocation.args() 表示方法的入参,比如这里的 args()[0] 就是 connection
        // StatementHandler 获取 MappedStatement 比较麻烦, 如果是 Exectour 的一些方法就很方便了, 因为参数就带上了,直接 args()[0]

        // 目标对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 目标对象可能是多层代理对象,拿到真正的目标对象,使用 mybatis 提供的一个工具类 MetaObject 获取
        Object o = realTarget(statementHandler);
        // 拿到真正的目标对象后再次通过 MetaObject 封装一下方便获取一些未提供 getter、setter 方法的属性
        MetaObject metaObject = SystemMetaObject.forObject(o);
        // 拿到 MappedStatement 对象
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 非查询不拦截
        if (!mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
            return invocation.proceed();
        }

        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);

        // 反射找到目标方法
        Method targetM = null;
        Class<?> clazz = Class.forName(className);
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            String name = method.getName();
            if (name.equals(methodName)){
                targetM = method;
                break;
            }
        }
        if (targetM == null){
            return invocation.proceed();
        }

        // 判断方法是否有注解
        boolean needPermission = targetM.isAnnotationPresent(Permission.class);
        if (needPermission) {
            BoundSql boundSql = statementHandler.getBoundSql();
            // 获取到原始 sql 语句
            String sql = boundSql.getSql();
            String showSql = showSql(mappedStatement.getConfiguration(), boundSql);
            System.out.println("原始执行 sql ==> " + showSql);

            // 新的 sql(模拟权限,增加实例id)
            String newSql = sql + " and  id > 0";

            // 通过反射修改 sql语句
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, newSql);

            String newShowSql = showSql(mappedStatement.getConfiguration(), boundSql);
            System.out.println("修改后的 sql ==> " + newShowSql);
        }
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 执行 sql
     *
     * @param configuration mybatis 配置对象
     * @param boundSql      boundSql
     * @return sql
     */
    private static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 参数
     *
     * @param obj 参数对象
     * @return 参数
     */
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
            value = value.replaceAll("\\\\", "\\\\\\\\");
            value = value.replaceAll("\\$", "\\\\\\$");
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "''";
            }

        }
        return value;
    }

    /**
     * 获取真实对象
     *
     * @param target 代理对象
     * @return 真实对象
     */
    public static Object realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            if (metaObject.hasGetter("h.target")) {
                Object value = metaObject.getValue("h.target");
                return realTarget(value);
            }
        }
        return target;
    }
}

插件注册

springboot 注册方式

@Configuration
public class MybatisPlugin {

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @PostConstruct
    public void addInterceptor() throws NoSuchFieldException, IllegalAccessException {
        sqlSessionFactory.getConfiguration().addInterceptor(new MybatisInterceptorUpdateSql());
    }

}

xml 注册方式

<plugins>
    <plugin interceptor="com.xiaolyuh.mybatis.EditPlugin">
        <property name="args1" value="参数示例"/>
    </plugin>
</plugins>
posted @ 2023-07-24 16:23  黄光跃  阅读(23)  评论(0编辑  收藏  举报