zoukankan      html  css  js  c++  java
  • Mybatis插件原理和PageHelper结合实战分页插件(七)

    今天和大家分享下mybatis的一个分页插件PageHelper,在讲解PageHelper之前我们需要先了解下mybatis的插件原理。PageHelper

    的官方网站:https://github.com/pagehelper/Mybatis-PageHelper



    一、Plugin接口


    mybatis定义了一个插件接口org.apache.ibatis.plugin.Interceptor,任何自定义插件都需要实现这个接口PageHelper就实现了改接口


    package org.apache.ibatis.plugin;
    
    import java.util.Properties;
    
    /**
     * @author Clinton Begin
     */
    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
    
      Object plugin(Object target);
    
      void setProperties(Properties properties);
    
    }



    1:intercept 拦截器,它将直接覆盖掉你真实拦截对象的方法。

    2:plugin方法它是一个生成动态代理对象的方法

    3:setProperties它是允许你在使用插件的时候设置参数值。


    看下com.github.pagehelper.PageHelper分页的实现了那些



        /**
         * Mybatis拦截器方法
         *
         * @param invocation 拦截器入参
         * @return 返回执行结果
         * @throws Throwable 抛出异常
         */
        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);
            }
        }


    这个方法获取了是分页核心代码,重新构建了BoundSql对象下面会详细分析

        /**
          * 只拦截Executor
          *
          * @param target
          * @return
          */
         public Object plugin(Object target) {
             if (target instanceof Executor) {
                 return Plugin.wrap(target, this);
             } else {
                 return target;
             }
         }


    这个方法是正对Executor进行拦截


        /**
         * 设置属性值
         *
         * @param p 属性值
         */
        public void setProperties(Properties p) {
            checkVersion();
            //多数据源时,获取jdbcurl后是否关闭数据源
            String closeConn = p.getProperty("closeConn");
            //解决#97
            if(StringUtil.isNotEmpty(closeConn)){
                this.closeConn = Boolean.parseBoolean(closeConn);
            }
            //初始化SqlUtil的PARAMS
            SqlUtil.setParams(p.getProperty("params"));
            //数据库方言
            String dialect = p.getProperty("dialect");
            String runtimeDialect = p.getProperty("autoRuntimeDialect");
            if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
                this.autoRuntimeDialect = true;
                this.autoDialect = false;
                this.properties = p;
            } else if (StringUtil.isEmpty(dialect)) {
                autoDialect = true;
                this.properties = p;
            } else {
                autoDialect = false;
                sqlUtil = new SqlUtil(dialect);
                sqlUtil.setProperties(p);
            }
        }



    基本的属性设置



    二、Plugin初始化


    初始化和所有mybatis的初始化一样的在之前的文章里面已经分析了 《Mybatis源码分析之SqlSessionFactory(一)》


    private void pluginElement(XNode parent) throws Exception {  
      if (parent != null) {  
        for (XNode child : parent.getChildren()) {  
          String interceptor = child.getStringAttribute("interceptor");  
          Properties properties = child.getChildrenAsProperties();  
          Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();  
          interceptorInstance.setProperties(properties);  
          configuration.addInterceptor(interceptorInstance);  
        }  
      }  
    }


    这里是讲多个实例化的插件对象放入configuration,addInterceptor最终存放到一个list里面的,以为这可以同时存放多个Plugin



    三、Plugin拦截


    插件可以拦截mybatis的4大对象ParameterHandler、ResultSetHandler、StatementHandler、Executor,源码如下图

    在Configuration类里面可以找到

    PageHelper使用了Executor进行拦截,上面的的源码里面已经可以看到了。

    我看下上图newExecutor方法

    executor = (Executor) interceptorChain.pluginAll(executor);


    这个是生产一个代理对象,生产了代理对象就运行带invoke方法




    四、Plugin运行


    mybatis自己带了Plugin方法,源码如下

    public class Plugin implements InvocationHandler {
      private Object target;
      private Interceptor interceptor;
      private Map<Class<?>, Set<Method>> signatureMap;
      private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
      }
      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;
      }
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          Set<Method> 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);
        }
      }
      private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        // issue #251
        if (interceptsAnnotation == null) {
          throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
        }
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
        for (Signature sig : sigs) {
          Set<Method> methods = signatureMap.get(sig.type());
          if (methods == null) {
            methods = new HashSet<Method>();
            signatureMap.put(sig.type(), methods);
          }
          try {
            Method method = sig.type().getMethod(sig.method(), sig.args());
            methods.add(method);
          } catch (NoSuchMethodException e) {
            throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
          }
        }
        return signatureMap;
      }
      private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
        Set<Class<?>> interfaces = new HashSet<Class<?>>();
        while (type != null) {
          for (Class<?> c : type.getInterfaces()) {
            if (signatureMap.containsKey(c)) {
              interfaces.add(c);
            }
          }
          type = type.getSuperclass();
        }
        return interfaces.toArray(new Class<?>[interfaces.size()]);
      }
    }

    wrap方法是为了生成一个动态代理类。

    invoke方法是代理绑定的方法,该方法首先判定签名类和方法是否存在,如果不存在则直接反射调度被拦截对象的方法,如果存在则调度插件的interceptor方法,这时候会初始化一个Invocation对象



    我们在具体看下PageHelper,当执行到invoke后程序将跳转到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方法


     /**
         * Mybatis拦截器方法,这一步嵌套为了在出现异常时也可以清空Threadlocal
         *
         * @param invocation 拦截器入参
         * @return 返回执行结果
         * @throws Throwable 抛出异常
         */
        public Object processPage(Invocation invocation) throws Throwable {
            try {
                Object result = _processPage(invocation);
                return result;
            } finally {
                clearLocalPage();
            }
        }


    继续跟进

       /**
         * Mybatis拦截器方法
         *
         * @param invocation 拦截器入参
         * @return 返回执行结果
         * @throws Throwable 抛出异常
         */
        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);
            }
        }


    这些都只是分装page方法,真正的核心是doProcessPage


      /**
         * Mybatis拦截器方法
         *
         * @param invocation 拦截器入参
         * @return 返回执行结果
         * @throws Throwable 抛出异常
         */
        private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
            //保存RowBounds状态
            RowBounds rowBounds = (RowBounds) args[2];
            //获取原始的ms
            MappedStatement ms = (MappedStatement) args[0];
            //判断并处理为PageSqlSource
            if (!isPageSqlSource(ms)) {
                processMappedStatement(ms);
            }
            //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
            ((PageSqlSource)ms.getSqlSource()).setParser(parser);
            try {
                //忽略RowBounds-否则会进行Mybatis自带的内存分页
                args[2] = RowBounds.DEFAULT;
                //如果只进行排序 或 pageSizeZero的判断
                if (isQueryOnly(page)) {
                    return doQueryOnly(page, invocation);
                }
                //简单的通过total的值来判断是否进行count查询
                if (page.isCount()) {
                    page.setCountSignal(Boolean.TRUE);
                    //替换MS
                    args[0] = msCountMap.get(ms.getId());
                    //查询总数
                    Object result = invocation.proceed();
                    //还原ms
                    args[0] = ms;
                    //设置总数
                    page.setTotal((Integer) ((List) result).get(0));
                    if (page.getTotal() == 0) {
                        return page;
                    }
                } else {
                    page.setTotal(-1l);
                }
                //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
                if (page.getPageSize() > 0 &&
                        ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                                || rowBounds != RowBounds.DEFAULT)) {
                    //将参数中的MappedStatement替换为新的qs
                    page.setCountSignal(null);
                    BoundSql boundSql = ms.getBoundSql(args[1]);
                    args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                    page.setCountSignal(Boolean.FALSE);
                    //执行分页查询
                    Object result = invocation.proceed();
                    //得到处理结果
                    page.addAll((List) result);
                }
            } finally {
                ((PageSqlSource)ms.getSqlSource()).removeParser();
            }
            //返回结果
            return page;
        }


    上面的有两个 Object result = invocation.proceed()执行,第一个是执行统计总条数,第二个是执行执行分页的查询的数据

    里面用到了代理。最终第一回返回一个总条数,第二个把分页的数据得到。





    五:PageHelper使用


    以上讲解了Mybatis的插件原理和PageHelper相关的内部实现,下面具体讲讲PageHelper使用

    1:先增加maven依赖:


    <dependency>  
        <groupId>com.github.pagehelper</groupId>  
        <artifactId>pagehelper</artifactId>  
        <version>4.1.6</version>  
    </dependency



    2:配置configuration.xml文件加入如下配置(plugins应该在environments的上面 )

    <plugins>
             <!-- PageHelper4.1.6 --> 
            <plugin interceptor="com.github.pagehelper.PageHelper">
                <property name="dialect" value="mysql"/>
                <property name="offsetAsPageNum" value="false"/>
                <property name="rowBoundsWithCount" value="false"/>
                <property name="pageSizeZero" value="true"/>
                <property name="reasonable" value="false"/>
                <property name="supportMethodsArguments" value="false"/>
                <property name="returnPageInfo" value="none"/>
            </plugin>
        </plugins>

    相关字段说明可以查看SqlUtilConfig源码里面都用说明


    注意配置的时候顺序不能乱了否则报错

    Caused by: org.apache.ibatis.builder.BuilderException: Error creating document instance.  Cause: org.xml.sax.SAXParseException; lineNumber: 57; columnNumber: 17; 元素类型为 "configuration" 的内容必须匹配 "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,plugins?,environments?,databaseIdProvider?,mappers?)"。
    at org.apache.ibatis.parsing.XPathParser.createDocument(XPathParser.java:259)
    at org.apache.ibatis.parsing.XPathParser.<init>(XPathParser.java:120)
    at org.apache.ibatis.builder.xml.XMLConfigBuilder.<init>(XMLConfigBuilder.java:66)
    at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:49)
    ... 2 more

    意思是配置里面的节点顺序是properties->settings->typeAliases->typeHandlers->objectFactory->objectWrapperFactory->plugins->environments->databaseIdProvider->mappers   plugins应该在environments之前objectWrapperFactory之后  这个顺序不能乱了



    3:具体使用

      1:分页


      SqlSession sqlSession = sessionFactory.openSession();
       UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
       PageHelper.startPage(1,10,true);  //第一页 每页显示10条
       Page<User> page=userMapper.findUserAll();

      2:不分页

     PageHelper.startPage(1,-1,true);

      3:查询总条数

    PageInfo<User>  info=new PageInfo<>(userMapper.findUserAll());





  • 相关阅读:
    【故障处理】ORA-12162: TNS:net service name is incorrectly specified (转)
    android studio 编程中用到的快捷键
    java时间格式串
    android Error occurred during initialization of VM Could not reserve enough space for object heap Could not create the Java virtual machine.
    linux安装vmware
    x1c 2017 安装mint18的坑——grub2
    x1c2017 8G版 win linux的取舍纠结记录
    python的try finally (还真不简单)
    kafka+docker+python
    json文件不能有注释
  • 原文地址:https://www.cnblogs.com/jeffen/p/6286335.html
Copyright © 2011-2022 走看看