• Mybatis拦截器


    在前面的文章 API开发(JWT数字签名) 一文中,我们利用SpringMVC 中的HandlerInterceptor接口用来拦截HTTP请求;而MyBatis里也提供了一个拦截器功能用来拦截sql语句。

    MyBatis插件介绍

    MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。

    MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性,从而影响SQL的生成、执行和返回结果,如:来影响Mapper.xml到SQL语句的生成、执行SQL前对预编译的SQL执行参数的修改、SQL执行后返回结果到Mapper接口方法返参POJO对象的类型转换和封装等。

    根据上面的对Mybatis拦截器作用的描述,可以分析其可能的用途;最常见的就是Mybatis自带的分页插件PageHelper或Rowbound参数,通过打印实际执行的SQL语句,发现我们的分页查询之前,先执行了COUNT(*)语句查询数量,然后再执行查询时修改了SQL语句即在我们写的SQL语句后拼接上了分页语句LIMIT(offset, pageSize);

    此外,实际工作中,可以使用Mybatis拦截器来做一些数据过滤、数据加密脱敏、SQL执行时间性能监控和告警等;既然要准备使用它,下面先来了解下其原理;

    默认情况下,MyBatis 允许使用插件来拦截的四种相关操作类方法:

    Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    ParameterHandler (getParameterObject, setParameters)
    ResultSetHandler (handleResultSets, handleOutputParameters)
    StatementHandler (prepare, parameterize, batch, update, query)
    
    • 1
    • 2
    • 3
    • 4

    这几个接口之间的关系大概是这样的:
    在这里插入图片描述

    Mybatis整体执行流程:

    在这里插入图片描述

    核心对象

    • Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
    • SqlSessionFactory:SqlSession工厂。
    • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
    • Executor:MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
    • StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
    • ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
    • ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
    • TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
    • MappedStatement:MappedStatement维护了一条节点的封装。
    • SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
    • BoundSql:表示动态生成的SQL语句以及相应的参数信息。

    MyBatis自定义插件的实现

    通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

    Interceptor 接口的定义如下所示:

    public interface Interceptor {
      //拦截器具体实现
      Object intercept(Invocation invocation) throws Throwable;
      //拦截器的代理类
      Object plugin(Object target);
      //添加属性
      void setProperties(Properties properties);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    相关注解:

    @Intercepts	// 描述:标志该类是一个拦截器
    @Signature 	// 描述:指明该拦截器需要拦截哪一个接口的哪一个方法
    
    // @Signature注解中属性:
    type; // 四种类型接口中的某一个接口,如Executor.class;
    method; // 对应接口中的某一个方法名,比如Executor的query方法;
    args; // 对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    下面来看一个自定义的简单Interceptor示例:

    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.reflection.DefaultReflectorFactory;
    import org.apache.ibatis.reflection.MetaObject;
    import org.apache.ibatis.reflection.SystemMetaObject;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.sql.Connection;
    import java.util.Properties;
    
    @Component
    //拦截StatementHandler类中参数类型为Statement的prepare方法(prepare=在预编译SQL前加入修改的逻辑)
    //即拦截 Statement prepare(Connection var1, Integer var2) 方法
    @Intercepts({
            @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
    })
    @Slf4j
    public class MyPlugin implements Interceptor  {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // 获取原始sql
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = statementHandler.getBoundSql();
            // 通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
            // 优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
            MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
            // 先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
            MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
    
            // 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并修改sql
            String mSql = sqlAnnotationEnhance(mappedStatement, boundSql);
    
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, mSql);
    
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            if (target instanceof StatementHandler) {
                return Plugin.wrap(target, this);
            } else {
                return target;
            }
        }
    
        @Override
        public void setProperties(Properties properties) {
        }
    
    
        /**
         * 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并增强sql
         * @param id 方法全路径
         * @param sqlCommandType sql类型
         * @param sql 所执行的sql语句
         */
        private String sqlAnnotationEnhance(MappedStatement mappedStatement, BoundSql boundSql) throws ClassNotFoundException {
            // 获取到原始sql语句
            String sql = boundSql.getSql().toLowerCase();
            // sql语句类型 select、delete、insert、update
            String sqlCommandType = mappedStatement.getSqlCommandType().toString();
    
            // 数据库连接信息
            //  Configuration configuration = mappedStatement.getConfiguration();
            //  ComboPooledDataSource dataSource = (ComboPooledDataSource)configuration.getEnvironment().getDataSource();
            //  dataSource.getJdbcUrl();
    
            // id为执行的mapper方法的全路径名,如com.cq.UserMapper.insertUser, 便于后续使用反射
            String id = mappedStatement.getId();
            // 获取当前所拦截的方法名称
            String mName = id.substring(id.lastIndexOf(".") + 1);
            // 通过类全路径获取Class对象
            Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
    
            // 获得参数集合
            String paramString = null;
            if (boundSql.getParameterObject() != null) {
                paramString = boundSql.getParameterObject().toString();
            }
    
            // 遍历类中所有方法名称,并匹配上当前所拦截的方法
            for (Method method : classType.getDeclaredMethods()) {
                if (mName.equals(method.getName())) {
                    // 判断方法上是否带有自定义@InterceptAnnotation注解
                    InterceptAnnotation interceptorAnnotation = method.getAnnotation(InterceptAnnotation.class);
                    if (interceptorAnnotation != null && interceptorAnnotation.flag()) {
                        log.info("intercept func:{}, type:{}, origin SQL:{}", mName, sqlCommandType, sql);
    
                        // 场景1:分页功能: return sql + " limit 1";
                        if ("select".equals(sqlCommandType.toLowerCase())) {
                            if (!sql.toLowerCase().contains("limit")) {
                                sql = sql + " limit 1";
                            }
                        }
    
                        // 场景2:校验功能 :update/delete必须要有where条件,并且打印出where中的条件
                        if ("update".equals((sqlCommandType.toLowerCase())) || "delete".equals(sqlCommandType.toLowerCase())) {
                            if (!sql.toLowerCase().contains("where")) {
                                log.warn("update or delete not safe!");
                            }
                        }
    
                        // 场景3:分表: 根据userId哈希,替换interceptorAnnotation注解中的表名为new_table(同一数据源)
                        if (sql.toLowerCase().contains(interceptorAnnotation.value())) {
                            String userId = getValue(paramString, "userId");
                            if (userId != null) {
                                int num = Integer.parseInt(userId);
                                // 相同数据源中,模拟分5个表
                                String new_table = interceptorAnnotation.value().concat("_").concat(String.valueOf(num % 5));
                                log.info("set table: {}", data_source_id, new_table);
                                // 替换sql表名
                                sql = StringUtils.replace(sql, interceptorAnnotation.value(), new_table);
                            }
                        }
    
    					// 场景4:分库分表: 路由到不同数据源,从参数获取dataSourceType
    					String sourceType = getValue(paramString, "dataSourceType");
    					if (sourceType != null) {
    						// mybatis多数据源配置参考:https://blog.csdn.net/qq_15437629/article/details/131961992
    						DynamicDataSourceContextHolder.setDataSourceType(sourceType);
    					} else {
    						// 也可以根据id 哈希选数据源
    					}
    
                        log.info("new SQL:{}", sql);
                        return sql;
                    }
                }
            }
            return sql;
        }
    
        String getValue(String param, String key) {
            if (param == null) {
                return null;
            }
            String[] keyValuePairs = param.substring(1, param.length() - 1).split(",");
            for (String pair : keyValuePairs) {
                String[] entry = pair.split("=");
                if (entry[0].trim().equals(key)) {
                    return entry[1].trim();
                }
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154

    自定义注解如下:

    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface InterceptAnnotation {
    
        String value() default "";
    
        /**
         * true增强、false忽略
         */
        boolean flag() default true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    添加插件:

    @Component
    public class DynamicPluginHelper {
    
    
        @Autowired
        private List<SqlSessionFactory> sqlSessionFactoryList;
    
        @Autowired
        private MyPlugin myPlugin;
       
        @PostConstruct
        public void addMysqlInterceptor() {
            for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
                org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
                configuration.addInterceptor(myPlugin);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    测试结果如下:

    sql.MyPlugin  : intercept func:findUser, type:SELECT, origin SQL:select * from t_user where id = ?
    sql.MyPlugin  : set table: t_user_4
    sql.MyPlugin  : new SQL:select * from t_user_4 where id = ? limit 1
    
    • 1
    • 2
    • 3

    问题记录

    错误描述:
    There is no getter for property named 'delegate' in 'class com.sun.proxy.$Proxy32'

    错误原因:
    1、你有多个拦截器,拦截同一对象的同一行为。测试时避免其他拦截器的干扰可以先把注册的拦截器注释掉。
    2、依赖包版本不对
    3、拦截器配置类放置的位置不正确,导致包没找到

    参考:
    https://blog.csdn.net/minghao0508/article/details/124420953
    https://blog.csdn.net/qq_36881887/article/details/111589294
    https://www.cnblogs.com/simplejavahome/p/16617112.html
    https://www.cnblogs.com/nefure/p/16948633.html
    https://blog.csdn.net/u011602668/article/details/128735771

  • 相关阅读:
    1004 成绩排名
    什么是Java?java是用来做什么的?
    网络基本概念
    基于深度学习的视觉三维重建研究总结
    基于机器学习之模型树短期负荷预测(Matlab代码实现)
    __wakeup绕过版本_PHP__wakeup()方法
    关于yolov8-class Pose(Detect)
    Java学习笔记2 变量的命名
    java98-线程join使用中断进行另一个
    Python 基础面试第三弹
  • 原文地址:https://blog.csdn.net/qq_15437629/article/details/133879678