MyBatis 经过提供插件机制,让咱们能够根据本身的须要去加强MyBatis 的功能。须要注意的是,若是没有彻底理解MyBatis 的运行原理和插件的工做方式,最好不要使用插件,由于它会改变系底层的工做逻辑,给系统带来很大的影响。html
MyBatis 的插件能够在不修改原来的代码的状况下,经过拦截的方式,改变四大核心对象的行为,好比处理参数,处理SQL,处理结果。git
第一个问题:github
不修改对象的代码,怎么对对象的行为进行修改,好比说在原来的方法前面作一点事情,在原来的方法后面作一点事情?spring
答案:你们很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。sql
第二个问题:apache
咱们能够定义不少的插件,那么这种全部的插件会造成一个链路,好比咱们提交一个休假申请,先是项目经理审批,而后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?设计模式
答案:插件是层层拦截的,咱们又须要用到另外一种设计模式——责任链模式。session
在以前的源码中咱们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,咱们应该了解MyBatis 容许哪些对象的哪些方法容许被拦截,并非每个运行的节点都是能够被修改的。只有清楚了这些对象的方法的做用,当咱们本身编写插件的时候才知道从哪里去拦截。在MyBatis 官网有答案,咱们来看一下:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins。mybatis
Executor 会拦截到CachingExcecutor 或者BaseExecutor。由于建立Executor 时是先建立CachingExcecutor,再包装拦截。从代码顺序上能看到。咱们能够经过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。app
在查看插件原理的前提上,咱们须要来看看官网对于自定义插件是怎么来作的,官网上有介绍:经过 MyBatis 提供的强大机制,使用插件是很是简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名便可。这里本人踩了一个坑,在Springboot中集成,同时引入了pagehelper-spring-boot-starter 致使RowBounds参数的值被刷掉了,也就是走到了个人拦截其中没有被设置值,这里须要注意,拦截器出了问题,能够Debug看一下Configuration配置类中拦截器链的包装状况。
@Intercepts({//须要拦截的方法 @Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), @Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} )}) public class MyPageInterceptor implements Interceptor { // 用于覆盖被拦截对象的原有方法(在调用代理对象Plugin 的invoke()方法时被调用) @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("将逻辑分页改成物理分页"); Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; // MappedStatement BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter RowBounds rb = (RowBounds) args[2]; // RowBounds // RowBounds为空,无需分页 if (rb == RowBounds.DEFAULT) { return invocation.proceed(); }// 在SQL后加上limit语句 String sql = boundSql.getSql(); String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit()); sql = sql + " " + limit; // 自定义sqlSource SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings()); // 修改原来的sqlSource Field field = MappedStatement.class.getDeclaredField("sqlSource"); field.setAccessible(true); field.set(ms, sqlSource); // 执行被拦截方法 return invocation.proceed(); } // target 是被拦截对象,这个方法的做用是给被拦截对象生成一个代理对象,并返回它 @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } // 设置参数 @Override public void setProperties(Properties properties) { } }
插件注册,在mybatis-config.xml 中注册插件:
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="offsetAsPageNum" value="true"/> ……后面所有省略…… </plugin> </plugins>
拦截签名跟参数的顺序有严格要求,若是按照顺序找不到对应方法会抛出异常:
org.apache.ibatis.exceptions.PersistenceException: ### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query
MyBatis 启动时扫描<plugins> 标签, 注册到Configuration 对象的 InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。
上面提到的能够被代理的四大对象都是何时被代理的呢?Executor 是openSession() 的时候建立的; StatementHandler 是SimpleExecutor.doQuery()建立的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的建立,建立以后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类建立。在咱们重写的 plugin() 方法里面能够直接调用returnPlugin.wrap(target, this);返回代理对象。
当个插件的状况下,代理能不能被代理?代理顺序和调用顺序的关系? 能够被代理。
由于代理类是Plugin,因此最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。能够经过invocation.proceed()调用到被代理对象被拦截的方法。
调用流程时序图:
先来看一下分页插件的简单用法:
PageHelper.startPage(1, 3); List<Blog> blogs = blogMapper.selectBlogById2(blog); PageInfo page = new PageInfo(blogs, 3);
对于插件机制咱们上面已经介绍过了,在这里咱们天然的会想到其所涉及的核心类 :PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,确定是要对咱们写的sql进行改写,那么必定是在 intercept 方法中进行操做的,咱们会发现这么一行代码:
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
调用到 AbstractHelperDialect 中的 getPageSql 方法:
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
// 获取sql String sql = boundSql.getSql();
//获取分页参数对象 Page page = this.getLocalPage(); return this.getPageSql(sql, page, pageKey); }
这里能够看到会去调用 this.getLocalPage(),咱们来看看这个方法:
public <T> Page<T> getLocalPage() { return PageHelper.getLocalPage(); } //线程独享 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); public static <T> Page<T> getLocalPage() { return (Page)LOCAL_PAGE.get(); }
能够发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取咱们所设置的 PageSize 与 PageNum,那么他是怎么设置值的呢?请看:
PageHelper.startPage(1, 3); public static <E> Page<E> startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, true); } public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } //设置页数,行数信息 setLocalPage(page); return page; } protected static void setLocalPage(Page page) {
//设置值 LOCAL_PAGE.set(page); }
在咱们调用 PageHelper.startPage(1, 3); 的时候,系统会调用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中能够获取到这个本地变量对象中的参数进行 SQL 的改写,因为改写有不少实现,咱们这里用的Mysql的实现:
在这里咱们会发现分页插件改写SQL的核心代码,这个代码就很清晰了,没必要过多赘述:
public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getPageSize()); } else { sqlBuilder.append(" LIMIT "); sqlBuilder.append(page.getStartRow()); sqlBuilder.append(","); sqlBuilder.append(page.getPageSize()); pageKey.update(page.getStartRow()); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
PageHelper 就是这么一步一步的改写了咱们的SQL 从而达到一个分页的效果。
关键类总结: