Mybatis源码分析之插件(plugins)源码详解

来源:互联网 发布:淘宝装饰店铺教程 编辑:程序博客网 时间:2024/06/05 22:57

         插件的实现就是使用动态代理。将mybytis四大对象(Executor、StatementHandler 、ParameterHandler、ResultHandler)在构造以后利用动态代理悄悄的将其代理,插入一些自己的逻辑,这里的动态代理是对实现类的动态代理,而不是像Dao层接口这种的直接对一个接口类的动态代理,理解起来也相对容易些。        

      插件实现过程中的一些关键类

    (1)Interceptor   自定义插件接口类,所有Mybatis插件必须实现这个类,统一管理,注册在mybatis_config里,会在初始化时候就加载到Configuration类里。
    (2)InterceptorChain   插件的缓存类,所有的插件都初始化到这个类中的一个缓存list中,四大对象构造时候将其包装
    (3)Plugin   Mybatis提供的一个实现InvocationHandler类,方便应用,统一到这里的invoke方法处理
     比较简单, 基本上就这么三个关键类。

     下面用一个经常使用的Mybats的分页插件为例子讲解插件从初始化到代理四大对象的整个过程:

@Intercepts(value=    {@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),  })  public class PageInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {  System.out.println("PageInterceptor -- intercept");                        if (invocation.getTarget() instanceof StatementHandler) {              StatementHandler statementHandler = (StatementHandler) invocation.getTarget();              MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, new DefaultObjectFactory(),                                                               new DefaultObjectWrapperFactory());              MappedStatement mappedStatement=(MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");            String selectId=mappedStatement.getId();            System.out.println(selectId);            if(selectId.matches(".*Page$"))            {               BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");                  // 分页参数作为参数对象parameterObject的一个属性                  String sql = boundSql.getSql();                Demo co=(Demo)(boundSql.getParameterObject());                                // 重写sql                  String countSql=concatCountSql(sql);                String pageSql=concatPageSql(sql,co);                                System.out.println("重写的 count  sql        :"+countSql);                System.out.println("重写的 select sql        :"+pageSql);                                Connection connection = (Connection) invocation.getArgs()[0];                                  java.sql.PreparedStatement countStmt = null;                  ResultSet rs = null;                  int totalCount = 0;                  try {                     countStmt = connection.prepareStatement(countSql);                      rs = countStmt.executeQuery();                      if (rs.next()) {                          totalCount = rs.getInt(1);                      }                                     } catch (SQLException e) {                      System.out.println("Ignore this exception"+e);                  } finally {                      try {                          rs.close();                          countStmt.close();                      } catch (SQLException e) {                          System.out.println("Ignore this exception"+ e);                      }                  }                                  metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);                                          //绑定count               co.setTotalRecord(totalCount);            }        }                 return invocation.proceed();}@Overridepublic Object plugin(Object target) {  if (target instanceof StatementHandler) {              return Plugin.wrap(target, this);          } else {              return target;          }  }@Overridepublic void setProperties(Properties properties) {// TODO Auto-generated method stub} public String concatCountSql(String sql){        StringBuffer sb=new StringBuffer("select count(*) from ");        sql=sql.toLowerCase();                if(sql.lastIndexOf("order")>sql.lastIndexOf(")")){            sb.append(sql.substring(sql.indexOf("from")+4, sql.lastIndexOf("order")));        }else{            sb.append(sql.substring(sql.indexOf("from")+4));        }        return sb.toString();    }        public String concatPageSql(String sql,Page co){        StringBuffer sb=new StringBuffer();        sb.append(sql);        sb.append(" limit ").append((co.getCurrentPage()-1)*co.getPageSize()).append(" , ").append(co.getPageSize());        return sb.toString();    }        public void setPageCount(){            }}
         例子也比较容易理解,实现Mybatis的插件接口,重写他的三个方法,plugin方法实现代理,intercept插入自己的操作,setProperties注入一些自己需要的属性。注解的意思是type表示准备要代理的类,method 代理类中准备要代理方法,args  代理方法的参数。
主要流程:
1.注册interceptor,会在初始化的时候实例化
2.sqlSession中4大对象在实例化的时候会调用pluginAll方法,该方法中会用interceptot创建代理
3.具体的,首先调用interceptor的plugin方法,该方法一般会使用Plugin类的wrap方法,Plugin类实现了invocationHandler,wrap中完成了代理创建
4.Plugin的invoke方法会调用intercptor的intercept方法,植入自己的逻辑,完成插件
5.执行完成以后,调用proceed方法,回归到原来的方法调用中


下面开始源代码的分析:

<plugins><plugin interceptor="com.yanzh.PageInterceptor"></plugin></plugins>
第一步将插件注册到Configuration。前面的初始化不说了,直接看进入到对插件节点的解析

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);      }    }  }
        看一下这个方法,拿到plugins节点,一次性可以注册多个plugin子节点。取到插件以后,将其利用反射实例化,插件的第三个方法会在这里被调用,然后直接注册到了configuration里了。

//Configuration里的注册方法,实际上是将其注入到一个interceptorChain里public void addInterceptor(Interceptor interceptor) {    interceptorChain.addInterceptor(interceptor);  }//interceptorChain里的方法,放到了interceptors缓存里public void addInterceptor(Interceptor interceptor) {    interceptors.add(interceptor);  }
     interceptors是一个list,private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 初始化就结束了,最终就是把插件注入到一个拦截器链的缓存List里。

   第二步看缓存中的拦截器是如何被执行的
           具体dao方法的执行流程就不讲解了,直接进入插件的相关过程,在sqlSessionFactory获取sqlSession的过程中调用openSession方法,这个流程中会初始化Executor对象。final Executor executor = configuration.newExecutor(tx, execType);就是这句(在DefaultSqlSessionFactory类中)

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;  }
       函数的前面是Mybatis几种不同的执行器,初始化的时候可以自己设置,如果不设置,默认即是SimpleExecutor,这些都与插件没有关系,看return前的最后一句,构建出Executor对象以后,不是立即返回,而是经历了pluginAll,玄机就在这里,看函数名也是添加所有插件。

public Object pluginAll(Object target) {    for (Interceptor interceptor : interceptors) {      target = interceptor.plugin(target);    }    return target;  }
        看到没有,这里就是把初始化时候缓存的拦截器都拿出来,一层层的包裹这个target对象(Executor),有几个拦截器就代理几次。最终会返回一个被包裹的对象,已经不是本身的Executor的对象了。被代理就是发生在plugin方法里。

       直接利用分页拦截器实例讲解,大多数都是这种模式。

@Overridepublic Object plugin(Object target) {  if (target instanceof StatementHandler) {              return Plugin.wrap(target, this);          } else {              return target;          }  }
     分页拦截器拦截的是StatementHandler对象,原理是一样的,假如此处的判断条件是target instanceof  Executro.直接利用mybatis提供的动态代理工具类来包装target.

 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;  }
         首先将@signature注解封装在一个map中,然后取到被代理类的.class类型,接着获取被代理类实现的接口,下面就是动态代理实现的真面目了,取接口,取类类型都是为代理准备参数。此处返回的就是一个被代理过的Executor对象。封装注解的函数就不说明了, 其实就是一个解析注解的过程,不熟悉的可以看下自定义注解,就是反射的知识,很容易看懂。那么自己准备要插入的逻辑(intercept函数)是怎么注入到代理中的呢。关键就在Plugin类的invoke方法中。

       再啰嗦几句,分析一下动态代理的几个要素,首先被代理类(target,也就是Executor),代理类实现的接口(getAllInterface方法返回了,jdk动态代理必须有接口类),实现InvocationHandler的类(Plugin类)。以后调用被代理类的方法的时候就会自动进入Plugin的invoke方法。

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);    }  }
         先取到注解中要拦截的方法,判断这次调用的被代理的方法是否在被拦截方法之列,不在的话直接反射调用被代理方法,在就调用拦截器的intercept方法,并且将被代理对象,方法,参数都封装在一个Invocation类中。

      看一下分页拦截器的方法,简单说一下。

public Object intercept(Invocation invocation) throws Throwable {  System.out.println("PageInterceptor -- intercept");                        if (invocation.getTarget() instanceof StatementHandler) {              StatementHandler statementHandler = (StatementHandler) invocation.getTarget();              MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, new DefaultObjectFactory(),                                                               new DefaultObjectWrapperFactory());              MappedStatement mappedStatement=(MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");            String selectId=mappedStatement.getId();            System.out.println(selectId);            if(selectId.matches(".*Page$"))            {               BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");                  // 分页参数作为参数对象parameterObject的一个属性                  String sql = boundSql.getSql();                Demo co=(Demo)(boundSql.getParameterObject());                                // 重写sql                  String countSql=concatCountSql(sql);                String pageSql=concatPageSql(sql,co);                                System.out.println("重写的 count  sql        :"+countSql);                System.out.println("重写的 select sql        :"+pageSql);                                Connection connection = (Connection) invocation.getArgs()[0];                                  java.sql.PreparedStatement countStmt = null;                  ResultSet rs = null;                  int totalCount = 0;                  try {                     countStmt = connection.prepareStatement(countSql);                      rs = countStmt.executeQuery();                      if (rs.next()) {                          totalCount = rs.getInt(1);                      }                                     } catch (SQLException e) {                      System.out.println("Ignore this exception"+e);                  } finally {                      try {                          rs.close();                          countStmt.close();                      } catch (SQLException e) {                          System.out.println("Ignore this exception"+ e);                      }                  }                                  metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);                                          //绑定count               co.setTotalRecord(totalCount);            }        }                 return invocation.proceed();}
              文中经过各种手段从被拦截对象中取到了即将要执行的sql语句和占位符参数,然后调用concatPageSql将分页参数append上去,偷天换日,最后一句又将换过的sql归还到原执行过程,invocation.proceed(),注意这句必须调用,不然归回不到原过程了,里面就是一个元方法的反射调用。

到这里一整套Mybatis的插件就走完了。sqlSession下的四大对象都是这么一个流程。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);    return statementHandler;  }public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);    return parameterHandler;  }public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,      ResultHandler resultHandler, BoundSql boundSql) {    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);    return resultSetHandler;  }
       一模一样的流程。插件整体比较简单,整个过程就是一个动态代理的实现过程,只要对动态代理了解,基本上没什么问题,不过还需要一些dao接口执行流程的熟悉知识,不然可能会感到一头雾水。







0 0
原创粉丝点击