一、mybatis执行过程和架构
· 加载
回顾前几文加载mybatis时,会通过sqlSessionFactoryBuilder的build方法对xml文件进行解析,解析成document树后,再依次对树中的XNode结点进行解析,如xml配置中的plugins、environments、mappers、typeHandlers等基础配置信息,初始化后赋值给configuration,解析结束。
最后通过sqlSessionFactoryBuilder初始化出来SqlSessionFactory,同时将configuration赋值给SqlSessionFactory,mybatis加载结束。
· sql执行过程
通过sqlSessionFactory创建出sqlSession,通过sqlSession操作数据库。首先会调用getMapper获取mapper,在加载时所有的mapper已经被注册到了mapperRegistry中,因此通过getMapper获取mapper时也就是直接从mapperRegistry中获取mapper动态代理工厂,通过mapper动态代理工厂获取mapper的动态代理对象。
根据mapper动态代理执行mapper方法前,会先查询缓存是否对该mapper方法进行解析过,如果缓存中存在则表示已经对mapper方法进行解析,直接返回。没有缓存则对mapper的方法进行解析,解析后的对象中存放的数据有sql标识id(即nameSpace路径或mapper接口文件路径名+方法名)、sql语句执行类型(select、delete、update、insert)、方法映射的基础配置信息(返回类型、是否为集合、是否为map、是否有返回参数等)。解析后放到缓存中。开始执行sql方法。
根据sql语句的执行类型执行对应的executor方法,selectOne和selectList都执行executor.query,delete、update、insert都执行executor.update。query方法执行时判断一级缓存是否存在,如果存在则直接返回,不存在则查库。update方法执行时会先清除当前sqlSession的一级缓存防止出现脏读现象。
最后执行数据库操作之前,生成parameterHandler处理器对象和resultHandler处理器对象并获取到所有的拦截器,执行拦截器中生成的动态代理的插件类的方法。最终开始进行sql预编译参数的填充,操作数据库,对结果集进行解析处理后返回,关闭连接资源,执行完毕。
· Executor架构
二、拦截器与插件原理
拦截器与插件核心的代码:
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//...
//初始化parameterHandler处理器时,会获取所有的拦截器
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//初始化resultSet处理器时,会获取所有的拦截器
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
· interceptionChain
获取所有的拦截器对象。
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); }return target;}
· 利用拦截器与插件原理进行sql分页参数的设置
(1)拦截器的设置
@Intercepts(@Signature(type = Executor.class, method = "query"
, args = {MappedStatement.class, Object.class, RowBounds.class
, ResultHandler.class}))
public class PageHelper implements Interceptor {
//...
}
使用PageHelper来帮助我们设置分页参数来代替我们在每条mapper.xml使用limit语句来进行分页。
上述分页拦截器只拦截Executor.class类,并且只拦截executor.query(mappedStatement,Object,rowBounds,resultHandler)方法。
拦截器除了使用拦截器注解,还需要实现Interceptor接口,并实现intercept、plugin、setProperties方法。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties);}
(2)获取拦截器
首先通过InterceptorChain会获取到所有的拦截器类。
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); }return target;}
会调用拦截器中的plugin方法,,比如pageHelper-分页拦截器中的plugin方法表示只拦截Executor类。
public Object plugin(Object target) {if (target instanceof Executor) {return Plugin.wrap(target, this); } else {return target; }
}
转换wrap的作用,实际是为该拦截器生成了一个动态代理类。
public static Object wrap(Object target, Interceptor interceptor) {
Map, Set> signatureMap = getSignatureMap(interceptor); Class> type = target.getClass(); Class>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) {return Proxy.newProxyInstance(
type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); }return target;}
(3)分页参数拦截器方法的执行
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {
Set 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); }
}
如果方法符合拦截条件,会先执行拦截方法,再执行基础方法。
三、拦截器与插件原理总结
通过Executor的基础架构可以看到,executor开始执行query或update时,会调用Configuration.newStatementHanler初始化StatementHandler,而在StatementHandler中,会先初始化ParameterHandler再初始化ResultSetHandler生成一个完整的StatementHandler。
要想对sql通过拦截器的方式设置分页参数的话,就可以在调用executor.query时,对该类需要增强的方法(query方法)增加拦截器与插件。
首先需要设置拦截器注解指定需要拦截的类和方法名与需要拦截的方法名的参数。其次需要将拦截的类实现interceptor接口,实现intercept、plugin、setProperties方法。
在对ParameterHander进行初始化时,会调用interceptor.pluginAll,获取所有满足条件的拦截器,返回被JDK动态代理的代理类。
最后在执行executor.query方法时,会执行代理类中的invoke,先执行拦截器中的intercept方法设置limit分页参数再执行基础方法,完成设置分页参数的拦截。
问题一:Executor三个子类的区别
(1)SimpleExecutor
一般都是用该执行器处理query和update操作。并且一次操作会打开一次Statement,操作完毕后关闭Statement资源。
(2)ResuseExecutor
处理query和update操作会重复使用Statement,不会关闭Statement资源。
(3)BatchExecutor
处理update操作,会将所有的sql添加到批处理中,进行统一执行。
问题二:简述Mybatis的插件运行原理
Mybatis可以编写针对ParameterHandeler、ResultSetHandler、StatementHandler、Executor这四种接口的插件。mybaits使用jdk动态代理,为这里需要拦截的接口生成代理对象实现接口方法拦截功能。每当执行拦截方法时,会进入调用动态代理中的invoke方法,执行拦截方法和基础方法。
问题三:如何编写一个插件
添加拦截器注解,指定拦截的类和方法,以及拦截方法的参数。拦截类需要实现intercept接口并复写plugin、intercept、setProperties方法。