Mybatis插件源码分析(含示例)

Mybatis插件源码分析(含示例)

对于mybatis插件首先得有一个使用上的认知,然后我们再去研究原理和源码会更加容易理解。

插件使用

网上有很多不错的博客,我摘取了一个给大家看看。感谢原博主。http://www.mybatis.cn/archives/81.html

概述

Mybatis插件又称拦截器,Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为(诸如SQL重写之类的),MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

总体概括为:
拦截执行器的方法
拦截参数的处理
拦截结果集的处理
拦截Sql语法构建的处理

Mybatis四大接口

既然Mybatis是对四大接口进行拦截的,那我们先要知道Mybatis的四大接口是哪些: Executor,StatementHandler, ResultSetHandler, ParameterHandler。

  • Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通ResultSetHandler进行自动映射,另外,它还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  • StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
  • ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
  • ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。

设计实现一个自定义插件

  1. 需求

把Mybatis所有执行的sql都记录下来。

  1. 代码实现

通过对org.apache.ibatis.executor.statement.StatementHandler 中的prepare 方法进行拦截即可。

prepare 方法签名如下:
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

自定义一个类,实现 org.apache.ibatis.pluginInterceptor 接口,代码如下:

package cn.mybatis.mybatis3.interceptor;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) })
public class SQLStatsInterceptor implements Interceptor {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        logger.info("mybatis intercept sql:{}", sql);
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
        String dialect = properties.getProperty("dialect");
        logger.info("mybatis intercept dialect:{}", dialect);
    }
}
  1. mybatis-config.xml 文件中增加plugins节点

    这样一个插件就开发完成了,接下来需要在 mybatis-config.xml 文件中增加plugins节点,完整配置如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
        <plugin interceptor="com.bytebeats.mybatis3.interceptor.SQLStatsInterceptor">
            <property name="dialect" value="mysql" />
        </plugin>
    </plugins>
</configuration>

源码分析

源码分析我将从插件的初始化和插件运行2个方向来展开。将涉及到的几个核心类Plugin、Interceptor、Intercepts、Signature。

插件的初始化

回顾一下我们之前源码分析第一章的内容,文章中提到了Configuration的初始化过程

org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
    org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)
    	org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
    		org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration()
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
        //插件初始化
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

这段代码肯定很熟悉,之前我们多次讲到过,“数据源、Mapper、缓存”的初始化都是在这里。这次我们主要看pluginElement(root.evalNode("plugins"));

  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).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  <plugins>
    <plugin interceptor="com.naiqing.mybatis.interceptor.SQLStatsInterceptor">
    </plugin>
  </plugins>

可以看出来java代码,循环所有的标签,解析其中的interceptor类名通过反射方法Class.getDeclaredConstructor().newInstance()实现对象的创建初始化。

所有的插件也被初始到Configration.interceptorChain中了,InterceptorChain我们来看下实现。

org.apache.ibatis.plugin.InterceptorChain
package org.apache.ibatis.plugin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * @author Clinton Begin
 */
public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

InterceptorChain的实现很简单,之所以把源码贴出来,是为了让你注意下里面的pluginAll(Object target)方法。它是为了将target通过代理模式串联所有插件。这个方法后面会有4次调用,分别对应mybatis可扩展的4个类。

插件加载

我们以更新update操作为例,插件加载

  1. Executor加载所有插件
SqlSession session = sqlSessionFactory.openSession()
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource()
    org.apache.ibatis.session.Configuration#newExecutor()
    
 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 = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
  1. StatementHandler加载所有插件
org.apache.ibatis.executor.BatchExecutor#doUpdate()
    org.apache.ibatis.session.Configuration#newStatementHandler()
  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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  1. ResultSetHandler加载所有插件
org.apache.ibatis.executor.BatchExecutor#doUpdate()
    org.apache.ibatis.session.Configuration#newStatementHandler()
    org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler()
    	org.apache.ibatis.executor.statement.SimpleStatementHandler#SimpleStatementHandler()
    org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler()
    org.apache.ibatis.session.Configuration#newParameterHandler()
  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 = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }
  1. ParameterHandler加载所有插件
org.apache.ibatis.executor.BatchExecutor#doUpdate()
    org.apache.ibatis.session.Configuration#newStatementHandler()
    org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler()
    	org.apache.ibatis.executor.statement.SimpleStatementHandler#SimpleStatementHandler()
    	org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler()
    org.apache.ibatis.session.Configuration#newResultSetHandler()
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 = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

通过上述的4段代码,我们看到了都是通过pluginAll()的方法将插件关联起来。之前我们看过InterceptorChain类的全部源码,其中pluginAll()的方法是遍历了所有的Interceptor,调用其plugin方法。我们来看下plugin方法的实现。其实这里就对应了我们自己扩展插件的实现了,但这个方法基本差不多,看下具体实现

com.naiqing.mybatis.interceptor.SQLStatsInterceptor
    com.naiqing.mybatis.interceptor.SQLStatsInterceptor#plugin()
 @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

我们再来看下

org.apache.ibatis.plugin.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;
  }

到这里我们可以看到,就是使用的jdk动态代理方式实现了插件的扩展。

最终达到的效果简单模拟就是


Executor=   ...<plugin3 <plugin2 <plugin1 <Executor>>>>

StatementHandler=   ...<plugin3 <plugin2 <plugin1 <StatementHandler>>>>

ResultSetHandler=   ...<plugin3 <plugin2 <plugin1 <ResultSetHandler>>>>

ParameterHandler=   ...<plugin3 <plugin2 <plugin1 <ParameterHandler>>>>
 

插件的加载就像洋葱一层一层包裹着原类。

我们可以看出来,在插件加载的时候是不区分的一股脑的全部加载上来,也就是说一个插件定义出来以后会被Executor、StatementHandler、ResultSetHandler、ParameterHandler同时加载。但一般情况下,我们都只是扩展其中一个类的某个方法,他是在哪里实现的呢?我们继续向下看。

Plugin类

这个类是Mybatis插件的核心实现类,把他看明白了你就知道插件是实现原理了。

下面是plugin.java的源码(去掉了部分不关心的代码)

package org.apache.ibatis.plugin;

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> 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注解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    ...
    //得到签名类信息(即需要扩展的类,也就是我们之前所说的Executor、StatementHandler、ResultSetHandler、ParameterHandler)
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) 
        //扩展类对应的方法名
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
		...
      }
    }
    return signatureMap;
  }

}
wrap()

首先我们来看下之前已经看过的方法wrap()

之所以要再看一遍这个方法,是因为上面我们只讲到了这个方法完成代理的作用,但没有介绍到另一个重要作用。

//得到自定义插件类的需要扩展的类和其方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

signatureMap作为一个plugin类的重要成员属性,作用就是装有自定义插件(即上面示例的SQLStatsInterceptor类)需要扩展的类和方法。在这里完成了初始化。

getSignatureMap()
org.apache.ibatis.plugin.Plugin#getSignatureMap()
  1. 拿到自定义插件的Intercepts注解。interceptor.getClass().getAnnotation(Intercepts.class);
  2. 得到签名类信息(即需要扩展的类,也就是我们之前所说的Executor、StatementHandler、ResultSetHandler、ParameterHandler)。Signature[] sigs = interceptsAnnotation.value()
  3. 得到方法名。signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());

我们再来对照之前编写的示例代码SQLStatsInterceptor.java

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) })

最终形成了一个需要扩展类方法的map,以扩展类Class(示例StatementHandler.class)为key,Set(示例prepare)为value。

invoke()

熟悉jdk动态代理的话,这个方法你应该不陌生。所有的被代理的类执行任意方法时都会先执行invoke方法。

根据我们上面的总结,在插件加载的时候,并不区分扩展类,但一般情况下,我们都只是扩展其中一个类的某个方法,他是在哪里实现的呢? 答案就在这个里。

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

通过getSignatureMap()方法得到扩展类的方法map的signatureMap,当每一次调用invoke方法的时候都会试图根据类名取到扩展方法(如果没取到,说明此方法不需要扩展),然后判断当前方法是否为需要扩展的方法,如果是则调用interceptor.intercept(),传入了target当前类,method方法,args参数。而intercept()是自定义的插件所具体的实现了。我们之前示例里面是这样的:

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();
    System.out.println("mybatis intercept sql:"+sql);
    return invocation.proceed();
  }

Invocation类

这个类就是用于传递代理类向我们自定义插件之间传递参数的,类本身很简单。只有3个属性,target当前被代理的类、method方法、args参数。这里我们主要是想说明下invocation.proceed()

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

这个方法的作用就是让我们的代理继续向下执行,比如说之前我们提到的代理关系

StatementHandler=   ...<plugin3 <plugin2 <plugin1 <StatementHandler>>>>

当我们调用需要扩展的方法如示例中的StatementHandler.prepare()方法时,首先是plugin3.invoke(),执行完成以后调用invocation.proceed()会传递到plugin2.invoke(),一直向下传最终调用StatementHandler.prepare()方法完成全部插件的调用。

插件流程图

插件初始化过程流程图

插件初始化过程流程图

插件调用流程图

以Executor插件为例

插件调用过程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值