Mybatis拦截器
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)
这几个接口之间的关系大概是这样的:

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维护了一条<select|update|delete|insert>节点的封装。
- 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);
}
相关注解:
@Intercepts // 描述:标志该类是一个拦截器
@Signature // 描述:指明该拦截器需要拦截哪一个接口的哪一个方法// @Signature注解中属性:
type; // 四种类型接口中的某一个接口,如Executor.class;
method; // 对应接口中的某一个方法名,比如Executor的query方法;
args; // 对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法;
下面来看一个自定义的简单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 {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取原始sqlStatementHandler 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的成员变量mappedStatementMappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");// 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并修改sqlString mSql = sqlAnnotationEnhance(mappedStatement, boundSql);Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, mSql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {if (target instanceof StatementHandler) {return Plugin.wrap(target, this);} else {return target;}}@Overridepublic 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、updateString 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哈希,替换注解中的表名if (sql.toLowerCase().contains(interceptorAnnotation.value())) {String userId = getValue(paramString, "userId");if (userId != null) {int num = Integer.parseInt(userId);// 模拟分10个库,5个表String data_source_id = String.valueOf(num % 10);String new_table = interceptorAnnotation.value().concat("_").concat(String.valueOf(num % 5));log.info("set data_source_id:{}, table: {}", data_source_id, new_table);// 设置data_source_id路由, 替换sql表名sql = StringUtils.replace(sql, interceptorAnnotation.value(), new_table);}}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;}
}
自定义注解如下:
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;
}
添加插件:
@Component
public class DynamicPluginHelper {@Autowiredprivate List<SqlSessionFactory> sqlSessionFactoryList;@Autowiredprivate MyPlugin myPlugin;@PostConstructpublic void addMysqlInterceptor() {for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();configuration.addInterceptor(myPlugin);}}
}
测试结果如下:
sql.MyPlugin : intercept func:findUser, type:SELECT, origin SQL:select * from t_user where id = ?
sql.MyPlugin : set data_source_id:4, table: t_user_4
sql.MyPlugin : new SQL:select * from t_user_4 where id = ? limit 1
问题记录
错误描述:
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
相关文章:
Mybatis拦截器
MyBatis插件介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。 MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性&…...
【项目设计】网络对战五子棋(上)
想回家过年… 文章目录 一、项目前置知识1. websocketpp库1.1 http1.0/1.1和websocket协议1.2 websocketpp库接口的前置认识1.3 搭建一个http/websocket服务器 2. jsoncpp库3. mysqlclient库 二、 项目设计1. 项目模块划分2. 实用工具类模块2.1 日志宏封装2.2 mysql_util2.3 j…...
【Overload游戏引擎细节分析】鼠标键盘控制摄像机原理
在上文中分析了摄像机类的实现,在计算投影视图矩阵时需要给摄像机输入其位置及转动四元数。这两个量一般通过鼠标键盘来控制,从而达到控制摄像机的目的。本文分析一下其控制原理。 Overload的摄像机控制实现在类CameraController中,其有三个个…...
VScode运行SVN拉下来的项目
安装依赖包 pnpm install 启动程序 查看package.json文件中的serve,根据这个启动 pnpm dev 在浏览器使用http://localhost:8848/访问...
jmeter集成kafka测试
Kafka的使用 查看kafka的topic ./kafka-topics --bootstrap-server 10.1.9.84:9092 --list 查看topic信息 ./kafka-topics --bootstrap-server 10.1.9.84:9092 --describe --topic topic_example_1 创建topic 创建topic名为test,分区数为8,副本数为…...
Java面试题-UDP\TCP\HTTP
UDP UDP特性 (1)UDP是无连接的:发送数据之前不需要像TCP一样建立连接,也不需要释放连接,所以减少了发送和接收数据的开销 (2)UDP 使用尽最大努力交付:即不保证可靠交付 ࿰…...
使用WPF模仿Windows记事本界面
本次仅模仿Windows记事本的模样,并未实现其功能。 所有代码如下: <Window x:Class"控件的基础使用.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/…...
【目标检测】Co-DETR:ATSS+Faster RCNN+DETR协作的先进检测器(ICCV 2023)
论文:DETRs with Collaborative Hybrid Assignments Training 代码**:https://github.com/Sense-X/Co-DETR 文章目录 摘要一、简介二、本文方法2.1.概述2.2.协同混合分配训练2.3. 定制的正 Query 生成2.4. Co-DETR为何有效1、丰富编码器的监督2、通过减少…...
iOS QQ登录SDK升级后报错Duplicate interface definition for class ‘TencentOAuth‘修复
起因 最近发现QQ登录SDK sdk-Lite3.3.8 TencentOpenAPI 在部分手机上会崩溃到初始化位置_tencentOAuth [[TencentOAuth alloc] initWithAppId:appid andDelegate:self];, 比如:iPhone6p 版本12.5.4,iPhone8p 版本14.1,iPad版本1…...
laravel中锁以及事务的简单使用
一、首先来说一下什么是共享锁?什么是排他锁? 共享:我可以读 写 加锁 , 别人可以 读 加锁。 排他:只有我 才 可以 读 写 加锁 , 也就是说,必须要等我提交事务,其他的才可以操作。 二、简单例子实现加锁 锁…...
Vue+openlayers+projs4实现坐标转换
一、背景 有一堆点数据,需要在地图上标记,只知参考北京54坐标系或西安80坐标系,但具体是哪种不清楚,这时候就需要坐标转换。ps:EPSG:3857(openlayers参照的坐标系) 二、思路 1、研…...
09 创建型模式-建造者模式
1.建造者模式介绍: 建造者模式 (builder pattern), 也被称为生成器模式 , 是一种创建型设计模式 定义: 将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不 同的表示。 2.建造者模式要解决的问题 建造者模式可以将部件和其组装过程分开&am…...
4.9 多协议标记交换MPLS
思维导图: 前言: **4.9 多协议标记交换MPLS笔记** 1. **定义与背景**: - MPLS (多协议标记交换) 是一种由 IETF 开发的新协议。 - “多协议”意味着 MPLS 的上层可以使用多种协议。 - 该协议综合了多家公司的技术,如 C…...
【经历】在职8个月->丰富且珍贵
在职8个月->丰富且珍贵 2021-3~2021-11:面试进入一家做400电话的公司,我进入公司时,加上我只有四个人(老板、人事、业务),开发只有我,所以:产品~设计~前端~后端~测试~上线~维护~培训,只有我自…...
使用GH(命令行)在本地提出Github上的issue、PR,合并PR
使用GH(命令行)在本地提出Github上的issue、PR,合并PR 前言 Github上的一些操作使用Git命令是无法完成的,因此正常流程就是在网页端进行。等一下,你让程序员用网页进行?果然,有命令行工具可以…...
@Scheduled定时器
Scheduled定时器 一、基本使用二、参数说明fixedDelayfixedRateinitialDelaycron 三、cron 表达式参数说明实用性的案例 四、Scheduled注意事项五、配置文件1、fixedDelay2、fixedRate3、cron 一、基本使用 Scheduled // 由Spring定义,用于将方法设置为调度任务。…...
Xshell+screen解决ssh连接 服务器掉线的问题
Linux screen命令解决SSH远程服务器训练代码断开连接后运行中断_linux screen ssh-CSDN博客 Linux命令之screen命令_linux screen_恒悦sunsite的博客-CSDN博客 使用教程: 这里粗略介绍一下 (1)xshell xftp(xshell点这个&#…...
coding_v3
面试经典 150 题 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台 数组/字符串 1.LC88【合并两个有序数组】 def solve(nums1, m, nums2, n):p1, p2 m-1, n-1tail m n -1while p1 > 0 or p2 > 0:if p1 -1:nums1[tail] nums2[p2]p2…...
Elasticsearch:什么是余弦相似度?
余弦相似度是数据科学、文本分析和机器学习领域的基本概念。 如果你想知道什么是余弦相似度或者它如何在现实世界的应用程序中使用,那么你来对地方了。 本指南旨在让你深入了解相似性是什么、其数学基础、优点及其在不同领域的各种应用。读完本指南后,你…...
【每日一题Day352】LC1726同积元组 | 哈希表+排列组合
同积元组【LC1726】 给你一个由 不同 正整数组成的数组 nums ,请你返回满足 a * b c * d 的元组 (a, b, c, d) 的数量。其中 a、b、c 和 d 都是 nums 中的元素,且 a ! b ! c ! d 。 思路 求出所有二元组的积及其出现次数,假设某个积出现的次…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看
文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...
逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...
系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文通过代码驱动的方式,系统讲解PyTorch核心概念和实战技巧,涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...
Xela矩阵三轴触觉传感器的工作原理解析与应用场景
Xela矩阵三轴触觉传感器通过先进技术模拟人类触觉感知,帮助设备实现精确的力测量与位移监测。其核心功能基于磁性三维力测量与空间位移测量,能够捕捉多维触觉信息。该传感器的设计不仅提升了触觉感知的精度,还为机器人、医疗设备和制造业的智…...
