MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。
不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。
我们可以定义很多的插件,那么这种所有的插件会形成一个链路,插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。
mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。
Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。
@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 中注册插件:
……后面全部省略……
拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:
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 启动时扫描
上面提到的可以被代理的四大对象都是什么时候被代理的呢?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); Listblogs = 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():
publicPage getLocalPage() { return PageHelper.getLocalPage(); } //线程独享 protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal(); public static Page getLocalPage() { return (Page)LOCAL_PAGE.get(); }
可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取所设置的 PageSize 与 PageNum,那么是怎么设置值的呢?
PageHelper.startPage(1, 3); public staticPage startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, true); } public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page 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 从而达到一个分页的效果。