学会自己编写Mybatis插件(拦截器)实现自定义需求
前言
你有了解过它是如何实现的吗?你有没有自己编写Mybatis插件去实现一些自定义需求呢?
插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或改变框架原有的功能。
Mybatis中也提供了插件的功能,虽然叫插件,但是实际上是通过拦截器(Interceptor)实现的,通过拦截某些方法的调用,在执行目标逻辑之前插入我们自己的逻辑实现。另外在MyBatis的插件模块中还涉及责任链模式和JDK动态代理
文章大纲:
一、应用场景一些字段的自动填充SQL语句监控、打印、数据权限等数据加解密操作、数据脱敏操作分页插件参数、结果集的类型转换
这些都是一些可以使用Mybatis插件实现的场景,当然也可以使用其他的方式来实现,只不过拦截的地方不一样罢了,有早有晚。二、Mybatis实现自定义拦截器
我们用自定义拦截器实现一个相对简单的需求,在大多数表设计中,都会有createtime和updatetime等字段,在创建或更新时需要更新相关字段。
如果是使用过MybatisPlus的小伙伴,可能知道在MybatisPlus中有一个自动填充功能,通过实现MetaObjectHandler接口中的方法来进行实现(主要的实现代码在com。baomidou。mybatisplus。core。MybatisParameterHandler中)。
但使用Mybatis,并没有相关的方法或API可以直接来实现。所以我们这次就用以此处作为切入点,自定义拦截器来实现类似的自动填充功能。
编写步骤编写一个拦截器类实现Interceptor接口添加拦截注解Intercepts在xml文件中配置拦截器或者添加到Configuration中
基础的环境我就不再贴出来啦哈,直接上三个步骤的代码2。1、编写拦截器packagecom。nzc。interceptor;importlombok。extern。slf4j。Slf4j;importorg。apache。ibatis。executor。Executor;importorg。apache。ibatis。executor。parameter。ParameterHandler;importorg。apache。ibatis。executor。resultset。ResultSetHandler;importorg。apache。ibatis。executor。statement。StatementHandler;importorg。apache。ibatis。mapping。MappedStatement;importorg。apache。ibatis。mapping。SqlCommandType;importorg。apache。ibatis。plugin。Interceptor;importorg。apache。ibatis。plugin。Intercepts;importorg。apache。ibatis。plugin。Invocation;importorg。apache。ibatis。plugin。Signature;importorg。springframework。beans。factory。annotation。Value;importjava。lang。reflect。Field;importjava。util。;author宁在春version1。0description:通过实现拦截器来实现部分字段的自动填充功能date20234621:49Intercepts({Signature(typeExecutor。class,methodupdate,args{MappedStatement。class,Object。class})})Slf4jpublicclassMybatisMetaInterceptorimplementsInterceptor{OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{MappedStatementmappedStatement(MappedStatement)invocation。getArgs()〔0〕;StringsqlIdmappedStatement。getId();log。info(sqlIdsqlId);SqlCommandTypesqlCommandTypemappedStatement。getSqlCommandType();Objectparameterinvocation。getArgs()〔1〕;log。info(sqlCommandTypesqlCommandType);log。info(拦截查询请求Executorupdate方法invocation。getMethod());if(parameternull){returninvocation。proceed();}if(SqlCommandType。INSERTsqlCommandType){Field〔〕fieldsgetAllFields(parameter);for(Fieldfield:fields){log。info(field。namefield。getName());try{注入创建时间if(createTime。equals(field。getName())){field。setAccessible(true);ObjectlocalcreateDatefield。get(parameter);field。setAccessible(false);if(localcreateDatenulllocalcreateDate。equals()){field。setAccessible(true);field。set(parameter,newDate());field。setAccessible(false);}}}catch(Exceptione){}}}if(SqlCommandType。UPDATEsqlCommandType){Field〔〕fieldsgetAllFields(parameter);for(Fieldfield:fields){log。info(field。namefield。getName());try{if(updateTime。equals(field。getName())){field。setAccessible(true);field。set(parameter,newDate());field。setAccessible(false);}}catch(Exceptione){e。printStackTrace();}}}returninvocation。proceed();}OverridepublicObjectplugin(Objecttarget){returnInterceptor。super。plugin(target);}稍后会展开说的OverridepublicvoidsetProperties(Propertiesproperties){System。out。println(begin);System。out。println(properties。getProperty(param1));System。out。println(properties。getProperty(param2));Interceptor。super。setProperties(properties);System。out。println(end);}获取类的所有属性,包括父类paramobjectreturnpublicstaticField〔〕getAllFields(Objectobject){Classlt;?clazzobject。getClass();ListFieldfieldListnewArrayList();while(clazz!null){fieldList。addAll(newArrayList(Arrays。asList(clazz。getDeclaredFields())));clazzclazz。getSuperclass();}Field〔〕fieldsnewField〔fieldList。size()〕;fieldList。toArray(fields);returnfields;}}2。2、添加到Mybatis配置
我这里使用的JavaConfig的方式packagecom。nzc。config;importcom。nzc。interceptor。;importorg。mybatis。spring。boot。autoconfigure。ConfigurationCustomizer;importorg。springframework。context。annotation。Bean;importorg。springframework。context。annotation。Configuration;ConfigurationpublicclassMyBatisConfig{BeanpublicConfigurationCustomizerconfigurationCustomizer(){returnnewConfigurationCustomizer(){Overridepublicvoidcustomize(org。apache。ibatis。session。Configurationconfiguration){开启驼峰命名映射configuration。setMapUnderscoreToCamelCase(true);MybatisMetaInterceptormybatisMetaInterceptornewMybatisMetaInterceptor();PropertiespropertiesnewProperties();properties。setProperty(param1,javaconfigvalue1);properties。setProperty(param2,javaconfigvalue2);mybatisMetaInterceptor。setProperties(properties);configuration。addInterceptor(mybatisMetaInterceptor);}};}}
如果是xml配置的话,则是如下:property是设置拦截器中需要用到的参数configurationpluginsplugininterceptorcom。nzc。interceptor。MybatisMetaInterceptorpropertynameparam1valuevalue1propertynameparam2valuevalue2pluginpluginsconfiguration2。3、测试
测试代码:实现了一个SysMapper的增删改查packagecom。nzc。mapper;importcom。nzc。entity。SysUser;importorg。apache。ibatis。annotations。Insert;importorg。apache。ibatis。annotations。Mapper;importorg。apache。ibatis。annotations。Select;importorg。apache。ibatis。annotations。Update;importjava。util。List;author宁在春description针对表【sysuser】的数据库操作MapperMapperpublicinterfaceSysUserMapper{Select(SELECTFROMtbsysuser)ListSysUserlist();Insert(insertintotbsysuser(id,username,realname,createtime,updatetime)values({id},{username},{realname},{createTime},{updateTime}))Booleaninsert(SysUsersysUser);Update(updatetbsysusersetusername{username},realname{realname},updatetime{updateTime}whereid{id})booleanupdate(SysUsersysUser);}author宁在春version1。0description:TODOdate20234621:38Slf4jRunWith(SpringRunner。class)SpringBootTestpublicclassSysUserMapperTest{AutowiredprivateSysUserMappersysUserMapper;Testpublicvoidtest1(){System。out。println(sysUserMapper。list());}Testpublicvoidtest2(){SysUsersysUsernewSysUser();sysUser。setId(1235);sysUser。setUsername(nzc5);sysUser。setRealname(nzc5);System。out。println(sysUserMapper。insert(sysUser));}Testpublicvoidtest3(){SysUsersysUsernewSysUser();sysUser。setId(1235);sysUser。setUsername(nzc7);sysUser。setRealname(nzc5);System。out。println(sysUserMapper。update(sysUser));}}
当然重点不在这里,而是在我们打印的日志上,一起来看看效果吧
此处相关日志对应Interceptor中的日志打印,想要了解的更为详细的可以debug查看一番。2。4、小结
通过这个小小的案例,我想大伙对于Mybatis中的拦截器应当是没有那般陌生了吧,接下来再来仔细聊聊吧如果你使用过MybatisPlus的话,在读完这篇博文后,可以思考思考下面这个问题,或去看一看源码,将知识串联起来,如果可以的话,记得把答案贴到评论区啦
思考:还记得这一小节开始我们聊到的MybatisPlus实现的自动填充功能吗?它是怎么实现的呢?三、拦截器接口介绍
MyBatis插件可以用来实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理publicinterfaceInterceptor{执行拦截逻辑的方法Objectintercept(Invocationinvocation)throwsThrowable;这个方法的参数target就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。该方法的实现很简单,只需要调用MyBatis提供的Plug类的wrap静态方法就可以通过Java动态代理拦截目标对象。defaultObjectplugin(Objecttarget){returnPlugin。wrap(target,this);}这个方法用来传递插件的参数,可以通过参数来改变插件的行为defaultvoidsetProperties(Propertiesproperties){NOP}}
有点懵没啥事,一个一个展开说:intercept方法Objectintercept(Invocationinvocation)throwsThrowable;
简单说就是执行拦截逻辑的方法,但不得不说这句话是个高度概括
首先我们要明白参数Invocation是个什么东东:publicclassInvocation{privatefinalObjecttarget;拦截的对象信息privatefinalMethodmethod;拦截的方法信息privatefinalObject〔〕args;拦截的对象方法中的参数publicInvocation(Objecttarget,Methodmethod,Object〔〕args){this。targettarget;this。methodmethod;this。argsargs;}get。。。利用反射来执行拦截对象的方法publicObjectproceed()throwsInvocationTargetException,IllegalAccessException{returnmethod。invoke(target,args);}}
联系我们之前实现的自定义拦截器上的注解:Intercepts({Signature(typeExecutor。class,methodupdate,args{MappedStatement。class,Object。class})})target对应我们拦截的Executor对象method对应Executorupdate方法args对应Executorupdateargs参数plugin方法
这个方法其实也很好说:
那就是Mybatis在创建拦截器代理时候会判断一次,当前这个类Interceptor到底需不需要生成一个代理进行拦截,如果需要拦截,就生成一个代理对象,这个代理就是一个{linkPlugin},它实现了jdk的动态代理接口{linkInvocationHandler},如果不需要代理,则直接返回目标对象本身加载时机:该方法在mybatis加载核心配置文件时被调用defaultObjectplugin(Objecttarget){returnPlugin。wrap(target,this);}publicclassPluginimplementsInvocationHandler{利用反射,获取这个拦截器MyInterceptor的注解Intercepts和Signature,然后解析里面的值,1先是判断要拦截的对象是哪一个2然后根据方法名称和参数判断要对哪一个方法进行拦截3根据结果做出决定,是返回一个对象呢还是代理对象publicstaticObjectwrap(Objecttarget,Interceptorinterceptor){MapClasslt;?,SetMethodsignatureMapgetSignatureMap(interceptor);Classlt;?typetarget。getClass();这边就是判断当前的interceptor是否包含在Classlt;?〔〕interfacesgetAllInterfaces(type,signatureMap);if(interfaces。length0){returnProxy。newProxyInstance(type。getClassLoader(),interfaces,newPlugin(target,interceptor,signatureMap));}如果不需要代理,则直接返回目标对象本身returntarget;}。。。。}setProperties方法
在拦截器中可能需要使用到一些变量参数,并且这个参数是可配置的,这个时候我们就可以使用这个方法啦,加载时机:该方法在mybatis加载核心配置文件时被调用defaultvoidsetProperties(Propertiesproperties){NOP}
关于如何使用:
javaConfig方式设置:BeanpublicConfigurationCustomizerconfigurationCustomizer(){returnnewConfigurationCustomizer(){Overridepublicvoidcustomize(org。apache。ibatis。session。Configurationconfiguration){开启驼峰命名映射configuration。setMapUnderscoreToCamelCase(true);MybatisMetaInterceptormybatisMetaInterceptornewMybatisMetaInterceptor();PropertiespropertiesnewProperties();properties。setProperty(param1,javaconfigvalue1);properties。setProperty(param2,javaconfigvalue2);mybatisMetaInterceptor。setProperties(properties);configuration。addInterceptor(mybatisMetaInterceptor);}};}
通过mybatisconfig。xml文件进行配置configurationpluginsplugininterceptorcom。nzc。interceptor。MybatisMetaInterceptorpropertynameparam1valuevalue1propertynameparam2valuevalue2pluginpluginsconfiguration
测试效果就是测试案例上那般,通过了解拦截器接口的信息,对于之前的案例不再是那般模糊啦
接下来再接着聊一聊拦截器上面那一坨注解信息是用来干嘛的吧,注意
当配置多个拦截器时,MyBatis会遍历所有拦截器,按顺序执行拦截器的plugin口方法,被拦截的对象就会被层层代理。
在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通invocationproceed()调用下层的方法,直到真正的方法被执行。
方法执行的结果从最里面开始向外层层返回,所以如果存在按顺序配置的三个签名相同的拦截器,MyBaits会按照CBAtarget。proceed()ABC的顺序执行。如果签名不同,就会按照MyBatis拦截对象的逻辑执行。
这也是我们最开始谈到的Mybatis插件模块所使用的设计模式责任链模式。四、拦截器注解介绍
上一个章节,我们只说明如何实现Interceptor接口来实现拦截,却没有说明要拦截的对象是谁,在什么时候进行拦截。就关系到我们之前编写的注解信息啦。Intercepts({Signature(typeExecutor。class,methodupdate,args{MappedStatement。class,Object。class})})
这两个注解用来配置拦截器要拦截的接口的方法。
Intercepts({})注解中是一个Signature()数组,可以在一个拦截器中同时拦截不同的接口和方法。
MyBatis允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis允许使用插件来拦截的接口包括以下几个。ExecutorParameterHandlerResultSetHandlerStatementHandler
Signature注解包含以下三个属性。type设置拦截接口,可选值是前面提到的4个接口method设置拦截接口中的方法名可选值是前面4个接口中所对应的方法,需要和接口匹配args设置拦截方法的参数类型数组通过方法名和参数类型可以确定唯一一个方法Executor接口
下面就是Executor接口的类信息publicinterfaceExecutor{intupdate(MappedStatementms,Objectparameter)throwsSQLException;EListEquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeycacheKey,BoundSqlboundSql)throwsSQLException;EListEquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException;ECursorEqueryCursor(MappedStatementms,Objectparameter,RowBoundsrowBounds)throwsSQLException;ListBatchResultflushStatements()throwsSQLException;voidcommit(booleanrequired)throwsSQLException;voidrollback(booleanrequired)throwsSQLException;CacheKeycreateCacheKey(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,BoundSqlboundSql);booleanisCached(MappedStatementms,CacheKeykey);voidclearLocalCache();voiddeferLoad(MappedStatementms,MetaObjectresultObject,Stringproperty,CacheKeykey,Classlt;?targetType);TransactiongetTransaction();voidclose(booleanforceRollback);booleanisClosed();voidsetExecutorWrapper(Executorexecutor);}
我只会简单说一些最常用的
1、updateintupdate(MappedStatementms,Objectparameter)throwsSQLException;
该方法会在所有的INSERT、UPDATE、DELETE执行时被调用,因此如果想要拦截这类操作,可以拦截该方法。接口方法对应的签名如下。Intercepts({Signature(typeExecutor。class,methodupdate,args{MappedStatement。class,Object。class})})
2、queryEListEquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,CacheKeycacheKey,BoundSqlboundSql)throwsSQLException;EListEquery(MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler)throwsSQLException;
该方法会在所有SELECT查询方法执行时被调用通过这个接口参数可以获取很多有用的信息,这也是最常被拦截的方法。Intercepts({Signature(typeExecutor。class,methodquery,args{MappedStatement。class,Object。class,RowBounds。class,ResultHandler。class}),Signature(typeExecutor。class,methodquery,args{MappedStatement。class,Object。class,RowBounds。class,ResultHandler。class,CacheKey。class,BoundSql。class})})
3、queryCursor:ECursorEqueryCursor(MappedStatementms,Objectparameter,RowBoundsrowBounds)throwsSQLException;
该方法只有在查询的返回值类型为Cursor时被调用。接口方法对应的签名类似于之前的。该方法只在通过SqlSession方法调用commit方法时才被调用voidcommit(booleanrequired)throwsSQLException;该方法只在通过SqlSessio口方法调用rollback方法时才被调用voidrollback(booleanrequired)throwsSQLException;该方法只在通过SqlSession方法获取数据库连接时才被调用,TransactiongetTransaction();该方法只在延迟加载获取新的Executor后才会被执行voidclose(booleanforceRollback);该方法只在延迟加载执行查询方法前被执行booleanisClosed();
注解的编写方法都是类似的。ParameterHandler接口publicinterfaceParameterHandler{该方法只在执行存储过程处理出参的时候被调用ObjectgetParameterObject();该方法在所有数据库方法设置SQL参数时被调用。voidsetParameters(PreparedStatementps)throwsSQLException;}
我都写一块啦,如果要拦截某一个的话只写一个即可Intercepts({Signature(typeParameterHandler。class,methodgetParameterObject,args{}),Signature(typeParameterHandler。class,methodsetParameters,args{PreparedStatement。class})})ResultSetHandler接口publicinterfaceResultSetHandler{该方法会在除存储过程及返回值类型为Cursor以外的查询方法中被调用。EListEhandleResultSets(Statementstmt)throwsSQLException;只会在返回值类型为ursor查询方法中被调用ECursorEhandleCursorResultSets(Statementstmt)throwsSQLException;只在使用存储过程处理出参时被调用,voidhandleOutputParameters(CallableStatementcs)throwsSQLException;}Intercepts({Signature(typeResultSetHandler。class,methodhandleResultSets,args{Statement。class}),Signature(typeResultSetHandler。class,methodhandleCursorResultSets,args{Statement。class}),Signature(typeResultSetHandler。class,methodhandleOutputParameters,args{CallableStatement。class})})StatementHandler接口publicinterfaceStatementHandler{该方法会在数据库执行前被调用优先于当前接口中的其他方法而被执行Statementprepare(Connectionconnection,IntegertransactionTimeout)throwsSQLException;该方法在prepare方法之后执行,用于处理参数信息voidparameterize(Statementstatement)throwsSQLException;在全局设置配置defaultExecutorTypeBATCH时,执行数据操作才会调用该方法voidbatch(Statementstatement)throwsSQLException;执行UPDATE、DELETE、INSERT方法时执行intupdate(Statementstatement)throwsSQLException;执行SELECT方法时调用,接口方法对应的签名如下。EListEquery(Statementstatement,ResultHandlerresultHandler)throwsSQLException;ECursorEqueryCursor(Statementstatement)throwsSQLException;获取实际的SQL字符串BoundSqlgetBoundSql();ParameterHandlergetParameterHandler();}Intercepts({Signature(typeStatementHandler。class,methodprepare,args{Connection。class,Integer。class}),Signature(typeStatementHandler。class,methodparameterize,args{Statement。class}),Signature(typeStatementHandler。class,methodbatch,args{Statement。class}),Signature(typeStatementHandler。class,methodupdate,args{Statement。class}),Signature(typeStatementHandler。class,methodquery,args{Statement。class,ResultHandler。class}),Signature(typeStatementHandler。class,methodqueryCursor,args{Statement。class}),Signature(typeStatementHandler。class,methodgetBoundSql,args{}),Signature(typeStatementHandler。class,methodgetParameterHandler,args{})}
如果有时间的话,我会更加建议看了的小伙伴,自己去实现接口做个测试,验证一番,也能了解的更彻底些。看会了,很多时候知识的记忆还是浅的。五、进一步思考
看完这篇文章后,不知道你有没有什么收获。
再次看看这张文章大纲的图吧
试着思考思考下面几个问题:Mybatis插件适用于哪些场景?回忆一下你做过的项目,是否有可以使用Mybatis插件来实现的呢?你可以编写一个Mybatis插件了吗?感兴趣的话,你可以试着去了解一下Mybatis分页插件的实现方式。
最后留下一个遇到的问题,也是下一篇文章可能会写的吧,同时也使用到了今天所谈到了的拦截器。
在项目中,你们都是如何针对表中某些字段进行加解密的呢?
链接:https:juejin。cnpost7220321558103097404