当前位置: 代码网 > it编程>编程语言>Java > MyBatis实现自定义MyBatis插件的流程详解

MyBatis实现自定义MyBatis插件的流程详解

2024年12月20日 Java 我要评论
初识插件我们在执行查询的时候,如果sql没有加上分页条件,数据量过大的话会造成内存溢出,因此我们可以通过mybatis提供的插件机制来拦截sql,并进行sql改写。mybatis的插件是通过动态代理来

初识插件

我们在执行查询的时候,如果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插件的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com