在最近的项目开发中遇到一个需求 需要对mysql做一些慢查询、大结果集等异常指标进行收集监控,从运维角度并没有对mysql进行统一的指标搜集,所以需要通过代码层面对指标进行收集,我采用的方法是通过mybatis的Interceptor拦截器进行指标收集在开发中出现了自定义拦截器 对于查询无法进行拦截的问题几经周折后终于解决,故进行记录学习,分享给大家下次遇到少走一些弯路;
像springmvc一样,mybatis也提供了拦截器实现,对Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了拦截器功能。
在使用时我们只需要 implements org.apache.ibatis.plugin.Interceptor类实现 方法头标注相应注解即可 如下代码会对CRUD的操作进行拦截:
- @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})
- })
- 复制代码
注解参数介绍:
拦截的类(type) | 拦截的方法(method) |
---|---|
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
StatementHandler | prepare, parameterize, batch, update, query |
ResultSetHandler | handleResultSets, handleOutputParameters |
官方代码示例:
- @Intercepts({@Signature(type = Executor.class, method = "query",
- args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
- public class TestInterceptor implements Interceptor {
- public Object intercept(Invocation invocation) throws Throwable {
- Object target = invocation.getTarget(); //被代理对象
- Method method = invocation.getMethod(); //代理方法
- Object[] args = invocation.getArgs(); //方法参数
- // do something ...... 方法拦截前执行代码块
- Object result = invocation.proceed();
- // do something .......方法拦截后执行代码块
- return result;
- }
- public Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
- }
- 复制代码
setProperties方法
因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。 如果我们的拦截器需要一些变量对象,而且这个对象是支持可配置的。 类似于Spring中的@Value("${}")从application.properties文件中获取。 使用方法:
mybatis-config.xml配置:
- <plugin interceptor="com.plugin.mybatis.MyInterceptor">
- <property name="username" value="xxx"/>
- <property name="password" value="xxx"/>
- plugin>
- 复制代码
方法中获取参数:properties.getProperty("username");
update类型操作可以正常拦截 query类型查询sql无法进入自定义拦截器,导致拦截失败以下为部分源码 由于涉及到公司代码以下代码做了mask的处理
自定义拦截器部分代码
- @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 SQLInterceptor implements Interceptor {
-
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- .....
- }
-
- @Override
- public Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
-
- @Override
- public void setProperties(Properties properties) {
- .....
- }
- }
- 复制代码
自定义拦截器拦截的是Executor执行器4参数query方法和update类型方法 由于mybatis的拦截器为责任链模式调用有一个传递机制 (第一个拦截器执行完向下一个拦截器传递 具体实现可以看一下源码)
update的操作执行确实进了自定义拦截器但是查询的操作始终进不来后通过追踪源码发现
pagehelper插件的 PageInterceptor 拦截器 会对 Executor执行器method=query 的4参数方法进行修改转化为 6参数方法 向下传递 导致执行顺序在pagehelper后面的拦截器的Executor执行器4参数query方法不会接收到传递过来的请求导致拦截器失效
PageInterceptor源码:
- /**
- * Mybatis - 通用分页拦截器
- *
- * GitHub: https://github.com/pagehelper/Mybatis-PageHelper
- *
- * Gitee : https://gitee.com/free/Mybatis_PageHelper
- *
- * @author liuzh/abel533/isea533
- * @version 5.0.0
- */
- @SuppressWarnings({"rawtypes", "unchecked"})
- @Intercepts(
- {
- @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
- @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
- }
- )
- public class PageInterceptor implements Interceptor {
- private volatile Dialect dialect;
- private String countSuffix = "_COUNT";
- protected Cache<String, MappedStatement> msCountMap = null;
- private String default_dialect_class = "com.github.pagehelper.PageHelper";
-
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- try {
- Object[] args = invocation.getArgs();
- MappedStatement ms = (MappedStatement) args[0];
- Object parameter = args[1];
- RowBounds rowBounds = (RowBounds) args[2];
- ResultHandler resultHandler = (ResultHandler) args[3];
- Executor executor = (Executor) invocation.getTarget();
- CacheKey cacheKey;
- BoundSql boundSql;
- //由于逻辑关系,只会进入一次
- if (args.length == 4) {
- //4 个参数时
- boundSql = ms.getBoundSql(parameter);
- cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
- } else {
- //6 个参数时
- cacheKey = (CacheKey) args[4];
- boundSql = (BoundSql) args[5];
- }
- checkDialectExists();
-
- List resultList;
- //调用方法判断是否需要进行分页,如果不需要,直接返回结果
- if (!dialect.skip(ms, parameter, rowBounds)) {
- //判断是否需要进行 count 查询
- if (dialect.beforeCount(ms, parameter, rowBounds)) {
- //查询总数
- Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
- //处理查询总数,返回 true 时继续分页查询,false 时直接返回
- if (!dialect.afterCount(count, parameter, rowBounds)) {
- //当查询总数为 0 时,直接返回空的结果
- return dialect.afterPage(new ArrayList(), parameter, rowBounds);
- }
- }
- resultList = ExecutorUtil.pageQuery(dialect, executor,
- ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
- } else {
- //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
- resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
- }
- return dialect.afterPage(resultList, parameter, rowBounds);
- } finally {
- if(dialect != null){
- dialect.afterAll();
- }
- }
- }
-
- /**
- * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
- *
- * 因此这里会出现 null 的情况 fixed #26
- */
- private void checkDialectExists() {
- if (dialect == null) {
- synchronized (default_dialect_class) {
- if (dialect == null) {
- setProperties(new Properties());
- }
- }
- }
- }
-
- private Long count(Executor executor, MappedStatement ms, Object parameter,
- RowBounds rowBounds, ResultHandler resultHandler,
- BoundSql boundSql) throws SQLException {
- String countMsId = ms.getId() + countSuffix;
- Long count;
- //先判断是否存在手写的 count 查询
- MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
- if (countMs != null) {
- count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
- } else {
- countMs = msCountMap.get(countMsId);
- //自动创建
- if (countMs == null) {
- //根据当前的 ms 创建一个返回值为 Long 类型的 ms
- countMs = MSUtils.newCountMappedStatement(ms, countMsId);
- msCountMap.put(countMsId, countMs);
- }
- count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
- }
- return count;
- }
-
- @Override
- public Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
-
- @Override
- public void setProperties(Properties properties) {
- //缓存 count ms
- msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
- String dialectClass = properties.getProperty("dialect");
- if (StringUtil.isEmpty(dialectClass)) {
- dialectClass = default_dialect_class;
- }
- try {
- Class> aClass = Class.forName(dialectClass);
- dialect = (Dialect) aClass.newInstance();
- } catch (Exception e) {
- throw new PageException(e);
- }
- dialect.setProperties(properties);
-
- String countSuffix = properties.getProperty("countSuffix");
- if (StringUtil.isNotEmpty(countSuffix)) {
- this.countSuffix = countSuffix;
- }
- }
-
- }
- }
- 复制代码
通过上述我们定位到了问题产生的原因 解决起来就简单多了 有俩个方案如下:
解决方案一 调整执行顺序
mybatis-config.xml 代码
我们的自定义拦截器配置的执行顺序是在PageInterceptor这个拦截器前面的(先配置后执行)
- <plugins>
-
- <plugin interceptor="com.github.pagehelper.PageInterceptor">
-
-
- <property name="reasonable" value="true"/>
-
- <property name="supportMethodsArguments" value="true"/>
-
- <property name="autoRuntimeDialect" value="true"/>
-
- plugin>
- <plugin interceptor="com.a.b.common.sql.SQLInterceptor"/>
- plugins>
- 复制代码
注意点!!!
pageHelper的依赖jar一定要使用pageHelper原生的jar包 pagehelper-spring-boot-starter jar包 是和spring集成的 PageInterceptor会由spring进行管理 在mybatis加载完后就加载了PageInterceptor 会导致mybatis-config.xml 里调整拦截器顺序失效
错误依赖:
- <dependency>-->
-
-
-
- dependency>
- 复制代码
正确依赖
- <dependency>
- <groupId>com.github.pagehelpergroupId>
- <artifactId>pagehelperartifactId>
- <version>5.1.10version>
- dependency>
- 复制代码
解决方案二 修改拦截器注解定义
- @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, CacheKey.class, BoundSql.class})
- })
- 复制代码
或者
- @Intercepts({
- @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
- @Signature(type = StatementHandler.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class})
- })