mybatis插件

2021-08-17  本文已影响0人  绮丽梦境

mybatis定义了一个接口 org.apache.ibatis.plugin.Interceptor,任何自定义插件都需要实现这个接口。原理类似于拦截器。

拦截范围

自定义插件可以拦截mybatis的4大对象ParameterHandler、ResultSetHandler、StatementHandler、Executor,但并不是所有的方法都可以拦截。

Interceptor 接口

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

Interceptor接口提供了三个方法。
1:intercept
拦截方法,里面是具体的拦截逻辑。通过参数Invocation 可获得拦截的对象、方法、参数。
2:plugin
接口提供默认实现,为拦截对象创建代理
3:setProperties
为拦截器设置属性

PageHelper实现原理

PageHelper 实现了Interceptor 接口,拦截Executor对象的query(MappedStatement mappedStatement, Object obj, RowBounds rowBounds, ResultHandler resultHandler) 方法

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {
  ……
}

测试代码

@Test
public void testPageHelper () throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    PageHelper.startPage(2,3);

    List<User> list = mapper.findAll();
    for (User user : list) {
        System.out.println(user);
    }

    sqlSession.close();
}

1.读取配置文件,解析plugins标签。为拦截器创建实例,添加到interceptorChain中

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

执行上面代码时,会解析配置文件,每个标签都进行解析,这里看解析plugins标签


image.png

配置文件里,plugins标签要解析的内容

<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //取出配置文件里plugin标签里的interceptor属性值,这里为com.github.pagehelper.PageHelper
        String interceptor = child.getStringAttribute("interceptor");
        //取出该interceptor的property
        Properties properties = child.getChildrenAsProperties();
        //通过反射创建拦截器实例,强转为Interceptor 类型
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        //为该拦截器实例设置属性
        interceptorInstance.setProperties(properties);
        //将拦截器实例添加到configuration中的interceptorChain中(ArrayList)
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

2.SqlSession实例化时,创建executor对象,然后在遍历plugins的时候,代理嵌套增强executor

SqlSession sqlSession = sqlSessionFactory.openSession();

Configuration中创建Executor,StatementHandler,parameterHandler,ResultSetHandler时调用对应的newXXX方法,方法中都会调用Configuration中的属性interceptorChains的pluginAll方法

创建executor时,调用pluginAll方法对其进行增强(JDK动态代理)

  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;
  }

循环对目标对象进行层层代理,生成最终的代理对象。

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

PageHelper重写了plugin方法,只拦截Executor

    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

Plugin的wrap方法,通过动态代理增强

  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> 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;
  }

Plugin类实现了 InvocationHandler 接口,覆盖了invoke方法。
当动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。
Plugin类的invoke方法的执行逻辑为:
如果是定义的拦截的方法 就执行拦截器的intercept方法;
不是需要拦截的方法 直接执行

3.PageHelper.startPage(2,3);

    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page<E> oldPage = SqlUtil.getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        SqlUtil.setLocalPage(page);
        return page;
    }

新建Page对象并初始化,并保存到ThreadLoacl中

public class SqlUtil implements Constant {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
    ……
}

ThreadLocal可以在指定线程内存取数据。每个线程都互不干扰。

4.List<User> list = mapper.findAll();
执行查询方法时,先动态创建mapper的代理类,然后底层会执行Executor的query方法,正是PageHelper要拦截的方法。所以此时程序会走到PageHelper的intercept方法中。

    public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

关键代码
sqlUtil.processPage(invocation);
在出现异常时也可以清空Threadlocal。这也是为什么调用PageHelper.startPage()方法后的第一个查询语句会分页而再次执行的查询语句不会分页。

public Object processPage(Invocation invocation) throws Throwable {
        try {
            Object result = _processPage(invocation);
            return result;
        } finally {
            clearLocalPage();
        }
    }

从本地线程中获取page

private Object _processPage(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        Page page = null;
        //支持方法参数时,会先尝试获取Page
        if (supportMethodsArguments) {
            page = getPage(args);
        }
        //分页信息
        RowBounds rowBounds = (RowBounds) args[2];
        //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
        if ((supportMethodsArguments && page == null)
                //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
                || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
            return invocation.proceed();
        } else {
            //不支持分页参数时,page==null,这里需要获取
            if (!supportMethodsArguments && page == null) {
                page = getPage(args);
            }
            return doProcessPage(invocation, page, args);
        }
    }

在doProcessPage(invocation, page, args) 方法中
1.新建查询数据总记录数的MappedStatement,放到缓存中
取出缓存中的ms,放行,获取到数据总记录数

2.还原ms,获取boundSql,设置参数后放行
放行后,执行Excutor的query方法


image.png

最终执行了MysqlParser 里的getPageSql方法,拼接了sql语句,然后去执行

public class MysqlParser extends AbstractParser {
    @Override
    public String getPageSql(String sql) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }

自定义分页插件

1.自定义一个类,实现Interceptor 接口,覆盖三个方法
2.在配置文件中配置插件

如下代码,实现以 "ByPager"结尾的方法,sql语句后拼接limit语句实现分页

package com.myown.interceptor;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

@Intercepts({@Signature(type= StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})
public class MyPageInterceptor implements Interceptor {
    private int page;
    private int size;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取拦截对象
        StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
        String mapId = mappedStatement.getId();
        if(mapId.matches(".+ByPager$")){
            ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
            Map<String, Object> params = (Map<String, Object>)parameterHandler.getParameterObject();
            page = (int)params.get("page");
            size = (int)params.get("size");
            String sql = (String) metaObject.getValue("delegate.boundSql.sql");
            sql += " limit "+(page-1)*size +","+size;
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
上一篇下一篇

猜你喜欢

热点阅读