问题背景
在最近的项目开发中遇到一个需求 需要对mysql做一些慢查询、大结果集等异常指标进行收集监控,从运维角度并没有对mysql进行统一的指标搜集,所以需要通过代码层面对指标进行收集,我采用的方法是通过mybatis的Interceptor拦截器进行指标收集在开发中出现了自定义拦截器 对于查询无法进行拦截的问题几经周折后终于解决,故进行记录学习,分享给大家下次遇到少走一些弯路;
mybatis拦截器使用
像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})})
注解参数介绍:
@Intercepts:标识该类是一个拦截器;
@Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法
Executor:提供了增删改查的接口 拦截执行器的方法.
StatementHandler:负责处理Mybatis与JDBC之间Statement的交互 拦截参数的处理.
ResultSetHandler:负责处理Statement执行后产生的结果集,生成结果列表 拦截结果集的处理.
ParameterHandler:是Mybatis实现Sql入参设置的对象 拦截Sql语法构建的处理。
官方代码示例:
@Intercepts({@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})publicclassTestInterceptorimplementsInterceptor{publicObjectintercept(Invocationinvocation)throwsThrowable{Objecttarget=invocation.getTarget();//被代理对象Methodmethod=invocation.getMethod();//代理方法Object[]args=invocation.getArgs();//方法参数//dosomething......方法拦截前执行代码块Objectresult=invocation.proceed();//dosomething.......方法拦截后执行代码块returnresult;}publicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}}
setProperties方法
因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。 如果我们的拦截器需要一些变量对象,而且这个对象是支持可配置的。 类似于Spring中的@Value("${}")从application.properties文件中获取。 使用方法:
mybatis-config.xml配置:
<plugininterceptor="com.plugin.mybatis.MyInterceptor"><propertyname="username"value="xxx"/><propertyname="password"value="xxx"/></plugin>
方法中获取参数:properties.getProperty("username");
bug内容:
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})})publicclassSQLInterceptorimplementsInterceptor{@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{.....}@OverridepublicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}@OverridepublicvoidsetProperties(Propertiesproperties){.....}}
自定义拦截器拦截的是Executor执行器4参数query方法和update类型方法 由于mybatis的拦截器为责任链模式调用有一个传递机制 (第一个拦截器执行完向下一个拦截器传递 具体实现可以看一下源码)
update的操作执行确实进了自定义拦截器但是查询的操作始终进不来后通过追踪源码发现
pagehelper插件的PageInterceptor 拦截器 会对Executor执行器method=query 的4参数方法进行修改转化为 6参数方法 向下传递 导致执行顺序在pagehelper后面的拦截器的Executor执行器4参数query方法不会接收到传递过来的请求导致拦截器失效
PageInterceptor源码:
/***Mybatis-通用分页拦截器*<p>*GitHub:https://github.com/pagehelper/Mybatis-PageHelper*<p>*Gitee:https://gitee.com/free/Mybatis_PageHelper**@authorliuzh/abel533/isea533*@version5.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}),})publicclassPageInterceptorimplementsInterceptor{privatevolatileDialectdialect;privateStringcountSuffix="_COUNT";protectedCache<String,MappedStatement>msCountMap=null;privateStringdefault_dialect_class="com.github.pagehelper.PageHelper";@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{try{Object[]args=invocation.getArgs();MappedStatementms=(MappedStatement)args[0];Objectparameter=args[1];RowBoundsrowBounds=(RowBounds)args[2];ResultHandlerresultHandler=(ResultHandler)args[3];Executorexecutor=(Executor)invocation.getTarget();CacheKeycacheKey;BoundSqlboundSql;//由于逻辑关系,只会进入一次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();ListresultList;//调用方法判断是否需要进行分页,如果不需要,直接返回结果if(!dialect.skip(ms,parameter,rowBounds)){//判断是否需要进行count查询if(dialect.beforeCount(ms,parameter,rowBounds)){//查询总数Longcount=count(executor,ms,parameter,rowBounds,resultHandler,boundSql);//处理查询总数,返回true时继续分页查询,false时直接返回if(!dialect.afterCount(count,parameter,rowBounds)){//当查询总数为0时,直接返回空的结果returndialect.afterPage(newArrayList(),parameter,rowBounds);}}resultList=ExecutorUtil.pageQuery(dialect,executor,ms,parameter,rowBounds,resultHandler,boundSql,cacheKey);}else{//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页resultList=executor.query(ms,parameter,rowBounds,resultHandler,cacheKey,boundSql);}returndialect.afterPage(resultList,parameter,rowBounds);}finally{if(dialect!=null){dialect.afterAll();}}}/***Springbean方式配置时,如果没有配置属性就不会执行下面的setProperties方法,就不会初始化*<p>*因此这里会出现null的情况fixed#26*/privatevoidcheckDialectExists(){if(dialect==null){synchronized(default_dialect_class){if(dialect==null){setProperties(newProperties());}}}}privateLongcount(Executorexecutor,MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{StringcountMsId=ms.getId()+countSuffix;Longcount;//先判断是否存在手写的count查询MappedStatementcountMs=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类型的mscountMs=MSUtils.newCountMappedStatement(ms,countMsId);msCountMap.put(countMsId,countMs);}count=ExecutorUtil.executeAutoCount(dialect,executor,countMs,parameter,boundSql,rowBounds,resultHandler);}returncount;}@OverridepublicObjectplugin(Objecttarget){returnPlugin.wrap(target,this);}@OverridepublicvoidsetProperties(Propertiesproperties){//缓存countmsmsCountMap=CacheFactory.createCache(properties.getProperty("msCountCache"),"ms",properties);StringdialectClass=properties.getProperty("dialect");if(StringUtil.isEmpty(dialectClass)){dialectClass=default_dialect_class;}try{Class<?>aClass=Class.forName(dialectClass);dialect=(Dialect)aClass.newInstance();}catch(Exceptione){thrownewPageException(e);}dialect.setProperties(properties);StringcountSuffix=properties.getProperty("countSuffix");if(StringUtil.isNotEmpty(countSuffix)){this.countSuffix=countSuffix;}}}}
解决方法:
通过上述我们定位到了问题产生的原因 解决起来就简单多了 有俩个方案如下:
调整拦截器顺序 让自定义拦截器先执行
自定义拦截器query方法也定义为 6参数方法或者不使用Executor.class执行器使用StatementHandler.class执行器也可以实现拦截
解决方案一 调整执行顺序
mybatis-config.xml 代码
我们的自定义拦截器配置的执行顺序是在PageInterceptor这个拦截器前面的(先配置后执行)
<plugins><!--com.github.pagehelper为PageHelper类所在包名--><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--使用下面的方式配置参数,后面会有所有的参数介绍--><!--reasonable:分页合理化参数,默认值为false。当该参数设置为true时,pageNum<=0时会查询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false时,直接根据参数进行查询。--><propertyname="reasonable"value="true"/><!--supportMethodsArguments:支持通过Mapper接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面params配置的字段中取值,查找到合适的值时就会自动分页。使用方法可以参考测试代码中的com.github.pagehelper.test.basic包下的ArgumentsMapTest和ArgumentsObjTest。--><propertyname="supportMethodsArguments"value="true"/><!--autoRuntimeDialect:默认值为false。设置为true时,允许在运行时根据多数据源自动识别对应方言的分页(不支持自动选择sqlserver2012,只能使用sqlserver),用法和注意事项参考下面的场景五--><propertyname="autoRuntimeDialect"value="true"/><!--params:为了支持startPage(Objectparams)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值,默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。--></plugin><plugininterceptor="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>--><!--<groupId>com.github.pagehelper</groupId>--><!--<artifactId>pagehelper-spring-boot-starter</artifactId>--><!--<version>1.2.12</version>--></dependency>
正确依赖
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.1.10</version></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})})
细节之中自有天地,整洁成就卓越代码。