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

         插件的实现就是使用动态代理。将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 {

	@Override
	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();
	}

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

	@Override
	public 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方法里。

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

@Override
	public 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接口执行流程的熟悉知识,不然可能会感到一头雾水。







猜你喜欢

转载自blog.csdn.net/ccityzh/article/details/71518137