(Mybatis学习笔记)Mybatis插件机制及源码分析

1.Mybatis插件介绍

在Mybatis中,它的四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)提供了简单易用的插件扩展机制,我们可以基于Mybatis插件机制实现分页、分表,监控等功能。而Mybatis对持久层的操作就是借助于四大核心对象。Mybatis支持用插件对四大核心对象进行拦截,对Mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的。换句话说,Mybatis中的操作的四大对象都是代理对象
在这里插入图片描述

Mybatis所允许拦截的方法如下

  • 执行器Executor (update、query、commit、rollback等方法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updatesquery等方法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters方法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);

2.Mybatis插件原理

在四大对象创建的时候:
1.每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
2.获取到所有的Interceptor(拦截器,即插件所需要实现的接口);调用interceptor.plugin(target);返回target包装后的对象;
3.插件机制,我们可以使用插件为目标对象创建一个代理对象;运用AOP(面向切面)我们的插件可以为四大对象创建出代理对 象,代理对象就可以拦截到四大对象的每一个执行。

拦截
插件具体是如何拦截并附加额外功能的呢?以ParameterHandler来说:


public ParameterHandler newParameterHandler(MappedStatement mappedStatement,Object object, BoundSql sql, InterceptorChain interceptorChain){
	ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
	parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);
	return parameterHandler;
}

public Object pluginAll(Object target) {
	for (Interceptor interceptor : interceptors) {
		target = interceptor.plugin(target);
	}
 	return target;
}

interceptorChain保存了所有的拦截器(interceptors),是Mybatis初始化的时候创建的。调用拦截器链中的拦截器依次对目标进行拦截和增强。interceptor.plugin(target)中的target可以理解为Mybatis中的四大对象。返回的target即是被重重代理后的对象。

那么如果我们想要拦截Executor的query方法,那么可以使用注解的方式定义插件:

@Intercepts({
 @Signature(
 type = Executor.class,	//被拦截的接口字节码
 method = "query",	//被拦截的方法名称
 args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})	//被拦截方法的入参字节码。如果方法重载,就能通过方法名和入参来确定唯一性
})
public class ExamplePlugin implements Interceptor {
 //省略逻辑
}

除此之外,我们还需将插件配置到sqlMapConfig.xml中

<plugins>
 <plugin interceptor="com.google.plugin.ExamplePlugin"></plugin>
</plugins>

这样Mybatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链)中。
待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory创建SqlSession,Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕之后,Mybatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在Executor相关方法被调用前执行。这就是Mybatis插件机制的基本原理。

3.自定义Mybatis插件

Mybatis的插件接口-Interceptor,所有插件类都要实现其中的3个方法:

  • intercept方法:插件的核心方法,只要被代理的目标对象的目标方法被调用时,每次都会执行intercept方法。
  • plugin方法:生成target的代理对象,并存到拦截器链中。
  • setProperties:传递插件所需要的参数,一般在plugin标签中配置属性。

设计一个自定义插件

@Intercepts({//大扩号表示可以定义多个@Signature对多个地方拦截,都用这个拦截器
	@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
	})
public class MyPlugin implements Interceptor {
	
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		System.out.println("对方法进行了增强");	//增强逻辑
        return invocation.proceed();	//执行原方法
	}
	
	@Override
    public Object plugin(Object target) {	//target为要拦截的对象
        Object wrap = Plugin.wrap(target, this);	//生成代理对象
        return wrap;
    }

	//插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
	@Override
    public void setProperties(Properties properties) {
        System.out.println("获取到的配置文件参数是:"+properties);
    }
}

mapper接口

public interface UserMapper {
	List<User> selectUser();
}

sqlMapConfig.xml

<plugins>
	<plugin interceptor="com.google.plugin.MyPlugin">
		<!--配置参数-->
		<property name="name" value="Bob"/>
	</plugin>
</plugins>

mapper.xml

<mapper namespace="com.google.mapper.UserMapper">
	<select id="selectUser" resultType="com.google.pojo.User">
		SELECT id,username FROM	user
	</select>
</mapper>

测试类

public class PluginTest {
@Test
public void test() throws IOException {
	InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
	SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
	SqlSession sqlSession = sqlSessionFactory.openSession();
	List<User> userList = sqlSession.selectList("com.google.mapper.IUserMapper.selectUser");
	userList.stream().forEach(System.out::println);
}

4.源码分析

执行插件逻辑
Plugin实现了InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	try {
		//获取被拦截的方法列表,比如signatureMap.get(Executor.class), 可能返回 [query,update,commit]
    	Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    	//检测方法列表是否包含被拦截的方法
    	if (methods != null && methods.contains(method)) {
    		//执行插件逻辑
        	return interceptor.intercept(new Invocation(target, method, args));
      	}
      	//执行被拦截的方法
      	return method.invoke(target, args);
   	} catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
}

invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截的方法是否配置在@Signature注解中,若是,则执行插件逻辑封装在intercept中,该方法的参数类型为Invocation invocation,主要用于存储目标类,方法以及方法参数列表。简单看看该类的定义:

public class Invocation {
	private final Object target;
	private final Method method;
	private final Object[] args;
	
	public Invocation(Object targetf Method method, Object[] args) {
		this.target = target;
		this.method = method;
		this.args = args;
	}
	
	//省略部分代码.....
	
	public Object proceed() throws InvocationTargetException,IllegalAccessException { 
		//调用被拦截的方法
		return method.invoke(target, args);
	}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值