美文网首页
mybatis插件

mybatis插件

作者: 绮丽梦境 | 来源:发表于2021-08-17 13:25 被阅读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) {
    
        }
    }
    

    相关文章

      网友评论

          本文标题:mybatis插件

          本文链接:https://www.haomeiwen.com/subject/dkqjbltx.html