初识插件
我们在执行查询的时候,如果sql没有加上分页条件,数据量过大的话会造成内存溢出,因此我们可以通过mybatis提供的插件机制来拦截sql,并进行sql改写。mybatis的插件是通过动态代理来实现的,并且会形成一个插件链。原理类似于拦截器,拦截我们需要处理的对象,进行自定义逻辑后,返回一个代理对象,进行下一个拦截器的处理。
我们先来看下一个简单插件的模板,首先要实现一个interceptor接口,并实现三个方法。并加上@intercepts注解。接下来我们以分页插件为例将对每个细节进行讲解。
/** * @classname : pageplugin * @description : 分页插件 * @date: 2020/12/29 */ @intercepts({}) public class pageplugin implements interceptor { private properties properties; @override public object intercept(invocation invocation) throws throwable { return invocation.proceed(); } @override public object plugin(object target) { return plugin.wrap(target, this); } @override public void setproperties(properties properties) { this.properties = properties; } }
拦截对象
在进行插件创建的时候,需要指定拦截对象。@intercepts
注解指定需要拦截的方法签名,内容是个signature
类型的数组,而signature
就是对拦截对象的描述。
@documented @retention(retentionpolicy.runtime) @target(elementtype.type) public @interface intercepts { /** * returns method signatures to intercept. * * @return method signatures */ signature[] value(); }
signature 需要指定拦截对象中方法的信息的描述。
@documented @retention(retentionpolicy.runtime) @target({}) public @interface signature { /** * 对象类型 */ class<?> type(); /** * 方法名 */ string method(); /** * 参数类型 */ class<?>[] args(); }
在mybatis中,我们只能对以下四种类型的对象进行拦截
- parameterhandler : 对sql参数进行处理
- resultsethandler : 对结果集对象进行处理
- statementhandler : 对sql语句进行处理
- executor : 执行器,执行增删改查
现在我们需要对sql进行改写,因此可以需要拦截executor的query方法进行拦截
@intercepts({@signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class, resulthandler.class})})
拦截实现
每个插件除了指定拦截的方法后,还需要实现interceptor
接口。interceptor
接口有以下三个方法。其中intercept是我们必须要实现的方法,在这里面我们需要实现自定义逻辑。其它两个方法给出了默认实现。
public interface interceptor { /** * 进行拦截处理 * @param invocation * @return * @throws throwable */ object intercept(invocation invocation) throws throwable; /** * 返回代理对象 * @param target * @return */ default object plugin(object target) { return plugin.wrap(target, this); } /** * 设置配置属性 * @param properties */ default void setproperties(properties properties) { // nop } }
因此我们实现intercept方法即可,因为我们要改写查询sql语句,因此需要拦截executor的query方法,然后修改rowbounds参数中的limit
,如果limit大于1000,我们强制设置为1000。
@slf4j @intercepts({@signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class , resulthandler.class})}) public class pageplugin implements interceptor { private properties properties; @override public object intercept(invocation invocation) throws throwable { object[] args = invocation.getargs(); rowbounds rowbounds = (rowbounds)args[2]; log.info("执行前, rowbounds = [{}]", jsonutil.tojsonstr(rowbounds)); if(rowbounds != null){ if(rowbounds.getlimit() > 1000){ field field = rowbounds.getclass().getdeclaredfield("limit"); field.setaccessible(true); field.set(rowbounds, 1000); } }else{ rowbounds = new rowbounds(0 ,100); args[2] = rowbounds; } log.info("执行后, rowbounds = [{}]", jsonutil.tojsonstr(rowbounds)); return invocation.proceed(); } @override public object plugin(object target) { return plugin.wrap(target, this); } @override public void setproperties(properties properties) { this.properties = properties; } }
加载流程
以上我们已经实现了一个简单的插件,在执行查询的时候对query方法进行拦截,并且修改分页参数。但是我们现在还没有进行插件配置,只有配置了插件,mybatis才能启动过程中加载插件。
xml配置插件
在mybatis-config.xml
中添加plugins标签,并且配置我们上面实现的plugin
<plugins> <plugin interceptor="com.example.demo.mybatis.pageplugin"> </plugin> </plugins>
xmlconfigbuilder加载插件
在启动流程中加载插件中使用到sqlsessionfactorybuilder的build方法,其中xmlconfigbuilder这个解析器中的parse()方法就会读取plugins标签下的插件,并加载configuration中的interceptorchain中。
// sqlsessionfactorybuilder public sqlsessionfactory build(inputstream inputstream, string environment, properties properties) { sqlsessionfactory var5; try { xmlconfigbuilder parser = new xmlconfigbuilder(inputstream, environment, properties); var5 = this.build(parser.parse()); } catch (exception var14) { throw exceptionfactory.wrapexception("error building sqlsession.", var14); } finally { errorcontext.instance().reset(); try { inputstream.close(); } catch (ioexception var13) { } } return var5; }
可见xmlconfigbuilder这个parse()方法就是解析xml中配置的各个标签。
// xmlconfigbuilder public configuration parse() { if (parsed) { throw new builderexception("each xmlconfigbuilder can only be used once."); } parsed = true; parseconfiguration(parser.evalnode("/configuration")); return configuration; } private void parseconfiguration(xnode root) { try { // issue #117 read properties first // 解析properties节点 propertieselement(root.evalnode("properties")); properties settings = settingsasproperties(root.evalnode("settings")); loadcustomvfs(settings); loadcustomlogimpl(settings); typealiaseselement(root.evalnode("typealiases")); // 记载插件 pluginelement(root.evalnode("plugins")); objectfactoryelement(root.evalnode("objectfactory")); objectwrapperfactoryelement(root.evalnode("objectwrapperfactory")); reflectorfactoryelement(root.evalnode("reflectorfactory")); settingselement(settings); // read it after objectfactory and objectwrapperfactory issue #631 environmentselement(root.evalnode("environments")); databaseidproviderelement(root.evalnode("databaseidprovider")); typehandlerelement(root.evalnode("typehandlers")); mapperelement(root.evalnode("mappers")); } catch (exception e) { throw new builderexception("error parsing sql mapper configuration. cause: " + e, e); } } xmlconfigbuilder 的pluginelement就是遍历plugins下的plugin加载到interceptorchain中。 // xmlconfigbuilder private void pluginelement(xnode parent) throws exception { if (parent != null) { // 遍历每个plugin插件 for (xnode child : parent.getchildren()) { // 读取插件的实现类 string interceptor = child.getstringattribute("interceptor"); // 读取插件配置信息 properties properties = child.getchildrenasproperties(); // 创建interceptor对象 interceptor interceptorinstance = (interceptor) resolveclass(interceptor).getdeclaredconstructor().newinstance(); interceptorinstance.setproperties(properties); // 加载到interceptorchain链中 configuration.addinterceptor(interceptorinstance); } } }
interceptorchain
是一个interceptor集合
,相当于是一层层包装,后一个插件就是对前一个插件的包装,并返回一个代理对象。
public class interceptorchain { private final list<interceptor> interceptors = new arraylist<>(); // 生成代理对象 public object pluginall(object target) { for (interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } // 将插件加到集合中 public void addinterceptor(interceptor interceptor) { interceptors.add(interceptor); } public list<interceptor> getinterceptors() { return collections.unmodifiablelist(interceptors); } }
创建插件对象
因为我们需要对拦截对象进行拦截,并进行一层包装返回一个代理类,那是什么时候进行处理的呢?以executor为例,在创建executor对象的时候,会有以下代码。
// configuration public executor newexecutor(transaction transaction, executortype executortype) { executortype = executortype == null ? defaultexecutortype : executortype; executortype = executortype == null ? executortype.simple : executortype; executor executor; if (executortype.batch == executortype) { executor = new batchexecutor(this, transaction); } else if (executortype.reuse == executortype) { executor = new reuseexecutor(this, transaction); } else { executor = new simpleexecutor(this, transaction); } if (cacheenabled) { executor = new cachingexecutor(executor); } // 创建插件对象 executor = (executor) interceptorchain.pluginall(executor); return executor; }
创建完executor对象后,就会调用interceptorchain.pluginall()
方法,实际调用的是每个interceptor的plugin()方法
。plugin()就是对目标对象的一个代理,并且生成一个代理对象返回。而plugin.wrap()
就是进行包装的操作。
// interceptor /** * 返回代理对象 * @param target * @return */ default object plugin(object target) { return plugin.wrap(target, this); }
plugin的wrap()主要进行了以下步骤:
- 获取拦截器拦截的方法,以拦截对象为key,拦截方法集合为value
- 获取目标对象的class对,比如executor对象
- 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口
- 创建代理类plugin对象,plugin实现了
invocationhandler
接口,最终对目标对象的调用都会调用plugin的invocate
方法。
// plugin public static object wrap(object target, interceptor interceptor) { // 获取拦截器拦截的方法,以拦截对象为key,拦截方法为value map<class<?>, set<method>> signaturemap = getsignaturemap(interceptor); // 获取目标对象的class对象 class<?> type = target.getclass(); // 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口 class<?>[] interfaces = getallinterfaces(type, signaturemap); // 如果对目标对象进行了拦截 if (interfaces.length > 0) { // 创建代理类plugin对象 return proxy.newproxyinstance( type.getclassloader(), interfaces, new plugin(target, interceptor, signaturemap)); } return target; }
例子
我们已经了解mybatis插件的配置,创建,实现流程,接下来就以一开始我们提出的例子来介绍实现一个插件应该做哪些。
确定拦截对象
因为我们要对查询sql分页参数进行改写,因此可以拦截executor的query方法,并进行分页参数的改写
@intercepts({@signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class , resulthandler.class})})
实现拦截接口
实现interceptor
接口,并且实现intercept
实现我们的拦截逻辑
@slf4j @intercepts({@signature(type = executor.class, method = "query", args = {mappedstatement.class, object.class, rowbounds.class , resulthandler.class})}) public class pageplugin implements interceptor { private properties properties; @override public object intercept(invocation invocation) throws throwable { object[] args = invocation.getargs(); rowbounds rowbounds = (rowbounds)args[2]; log.info("执行前, rowbounds = [{}]", jsonutil.tojsonstr(rowbounds)); if(rowbounds != null){ if(rowbounds.getlimit() > 1000){ field field = rowbounds.getclass().getdeclaredfield("limit"); field.setaccessible(true); field.set(rowbounds, 1000); } }else{ rowbounds = new rowbounds(0 ,100); args[2] = rowbounds; } log.info("执行后, rowbounds = [{}]", jsonutil.tojsonstr(rowbounds)); return invocation.proceed(); } @override public object plugin(object target) { return plugin.wrap(target, this); } @override public void setproperties(properties properties) { this.properties = properties; } }
配置插件
在mybatis-config.xml
中配置以下插件
<plugins> <plugin interceptor="com.example.demo.mybatis.pageplugin"> </plugin> </plugins>
测试
ttestusermapper.java
新增selectbypage方法
list<ttestuser> selectbypage(@param("offset") integer offset, @param("pagesize") integer pagesize);
mapper/ttestusermapper.xml
新增对应的sql
<select id="selectbypage" resultmap="baseresultmap"> select <include refid="base_column_list" /> from t_test_user <if test="offset != null"> limit #{offset}, #{pagesize} </if> </select>
最终测试代码,我们没有在查询的时候指定分页参数。
public static void main(string[] args) { try { // 1. 读取配置 inputstream inputstream = resources.getresourceasstream("mybatis-config.xml"); // 2. 创建sqlsessionfactory工厂 sqlsessionfactory sqlsessionfactory = new sqlsessionfactorybuilder().build(inputstream); // 3. 获取sqlsession sqlsession sqlsession = sqlsessionfactory.opensession(executortype.simple); // 4. 获取mapper ttestusermapper usermapper = sqlsession.getmapper(ttestusermapper.class); // 5. 执行接口方法 list<ttestuser> list2 = usermapper.selectbypage(null, null); system.out.println("list2="+list2.size()); // 6. 提交事物 sqlsession.commit(); // 7. 关闭资源 sqlsession.close(); inputstream.close(); } catch (exception e){ log.error(e.getmessage(), e); } }
最终打印的日志如下,我们可以看到rowbounds
已经被我们强制修改了只能查处1000条数据。
10:11:49.313 [main] info com.example.demo.mybatis.pageplugin - 执行前, rowbounds = [{"offset":0,"limit":2147483647}] 10:11:58.015 [main] info com.example.demo.mybatis.pageplugin - 执行后, rowbounds = [{"offset":0,"limit":1000}] 10:12:03.211 [main] debug org.apache.ibatis.transaction.jdbc.jdbctransaction - opening jdbc connection 10:12:04.269 [main] debug org.apache.ibatis.datasource.pooled.pooleddatasource - created connection 749981943. 10:12:04.270 [main] debug org.apache.ibatis.transaction.jdbc.jdbctransaction - setting autocommit to false on jdbc connection [com.mysql.cj.jdbc.connectionimpl@2cb3d0f7] 10:12:04.283 [main] debug com.example.demo.dao.ttestusermapper.selectbypage - ==> preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 10:12:04.335 [main] debug com.example.demo.dao.ttestusermapper.selectbypage - ==> parameters: list2=1000
以上就是mybatis实现自定义mybatis插件的流程详解的详细内容,更多关于mybatis自定义mybatis插件的资料请关注代码网其它相关文章!
发表评论