Mybatis 拦截器详解

1,251 阅读11分钟

1 Mybatis核心类和组件

1.1 核心类组件

image.png

1.1.1 配置对象Configuration

mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一个表。MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。

1.1.2 会话工厂 SqlSessionFactory

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心。 有两种方式获取SqlSessionFactory:
(1) 从 mybatis-config.xml 文件构建 SqlSessionFactory
示例

String resource = "org/mybatis/example/mybatis-config.xml"; 
InputStream inputStream = Resources.getResourceAsStream(resource); 
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

(2) 配置类构建 SqlSessionFactory
示例

DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); 
TransactionFactory transactionFactory = new JdbcTransactionFactory(); 
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

1.1.3 会话对象SqlSession

SqlSession是MyBatis工作的主要顶层API,表示和数据库交互时的会话。

SqlSession提供了在数据库执行 SQL 所需的所有方法,通过 SqlSession实例直接执行已映射的SQL语句

try (SqlSession session = sqlSessionFactory.openSession()) { 
BlogMapper mapper = session.getMapper(BlogMapper.class); 
Blog blog = mapper.selectBlog(101); 
}

1.1.4 执行器Executor

原生JDBC执行 SQL 语句的是 Statement 类,Executor可以理解是Mybatis 对 JDBC Statement类的封装,Executor 根据 SqlSession 传递的参数动态的生成需要执行的SQL语句,同时负责查询缓存的维护。

1.1.5 MappedStatement对象

MappedStatement是维护一条SQL节点的封装, 用于存储SQL语句的id,参数等信息 每一个 MappedStatement实例对应 mapper.xml 中配置的一个 SQL 语句。

1.1.6 输入参数映射

输入参数类型可以是 MapList 等集合类型,也可以是基本数据类型和 POJO 类型,输入参数映射过程类似于 JDBC 对 perparedStatement 对象设置参数的过程。

1.1.7 输出结果映射

输出结果类型可以是 MapList 等集合类型,也可以是基本数据类型和 POJO 类型,输出结果映射过程类似于 JDBC对结果集的解析过程。

1.2 SqlSession下的四大核心组件

MyBatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。SqlSession 是 MyBatis 的顶层接口,它提供了所有执行语句,获取映射器和管理事务等方法。

image.png

Mybatis中SqlSession下有四大核心组件:ParameterHandler 、ResultSetHandler 、StatementHandler 、Executor 。

1.2.1 Executor

对SqlSession方法的访问最终都会落到Executor相应方法上去。Executor负责生成动态 SQL 以及管理缓存。

普通Executor分三类

  • SimpleExecutor:简单类型的执行器,也是默认的执行器,每次执行update或者select操作,都会创建一个Statement对象,执行结束后关闭Statement对象。

  • ReuseExecutor:可重用的执行器,重用的是Statement对象,第一次执行一条sql,会将这条sql的Statement对象缓存在key-value结构的map缓存中。下一次执行,就可以从缓存中取出Statement对象,减少了重复编译的次数,从而提高了性能。每个SqlSession对象都有一个Executor对象,因此这个缓存是SqlSession级别的,所以当SqlSession销毁时,缓存也会销毁。

  • BatchExecutor:批量执行器,默认情况是每次执行一条sql,MyBatis都会发送一条sql。而批量执行器的操作是,每次执行一条sql,不会立马发送到数据库,而是批量一次性发送多条sql。

1.2.2 StatementHandler

StatementHandler 对象使用数据库中的Statement(PrepareStatement)执行操作,即底层是封装好的PrepareStatement。StatementHandler负责设置 Statement 对象中的查询参数、处理 JDBC 返回的 resultSet,将 resultSet 加工为 List 集合返回。

1.2.3 ParameterHandler

ParameterHandler 处理SQL参数,负责将传入的 Java 对象转换 JDBC 类型对象,并为 PreparedStatement 的动态 SQL 填充数值。

1.2.4 ResultSetHandler

ResultSetHandler负责结果集ResultSet封装处理返回。

1.3 组件生命周期

1.3.1 SqlSessionFactory

SqlSessionFactory在MyBatis应用的整个生命周期中,每个数据库只对应一个SqlSessionFactory,因此 SqlSessionFactory 的最佳作用域是应用作用域,以单例模式获取该对象。

1.3.2 SqlSession

SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。一个SqlSession可以执行多条SQL,保证事务的一致性。确保 SqlSession 使用后关闭。

try (SqlSession session = sqlSessionFactory.openSession()) {
// 应用逻辑代码 
}

1.3.3 Mapper

Mapper的作用是发送SQL,然后返回需要的结果。任何映射器Mapper实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。

  try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 应用逻辑代码
}

2 拦截器

拦截器主要是为开发者提供了一些额外的操作,可以让我们管理事务,操作SQL等。 自定义拦截器需要实现Interceptor接口, 并在接口上添加@Intercepts注解

2.1 @Intercepts注解

@Intercepts:标识该类是一个拦截器。下面是Mybatis官网给的一个例子

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class})
})

2.1.1 拦截注解@Signature

@Signature拦截器相关属性设置

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
 /**
  * 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
  */
  Class<?> type();
    
  /**
   * 在定义拦截类的基础之上,在定义拦截的方法
   */
  String method();
   /**
    * 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
    * 因方法里面可能重载,不指定参数列表,不能确定是对应拦截的方法
    */
  Class<?>[] args();
}

2.1.2 拦截类型type

拦截器的类型,总共有4种,按照执行的先后顺序为:

  • Executor:拦截执行器的方法
  • ParameterHandler:拦截参数的处理
  • ResultHandler:拦截结果集的处理
  • StatementHandler:拦截Sql语法构建的处理

2.1.3 拦截方法method

基于4种拦截器类型type,拦截方法method有下列

  • Executor

    • update - 更新操作
    • query - 查询操作
    • flushStatements - flush操作
    • commit - 提交事务
    • rollback - 回滚事务
    • getTransaction - 获取事务管理对象
    • close - 关闭
    • isClosed - 链接是否已关闭
  • ParameterHandler

    • getParameterObject - 获取参数对象
    • setParameters - 设置参数
  • ResultSetHandler

    • handleResultSets - 处理结果集
    • handleOutputParameters - 处理输出参数
  • StatementHandler

    • prepare - 预编译
    • parameterize - 参数化设置
    • batch - 批量操作
    • update - 更新
    • query - 查询

2.1.4 参数类型args

Executorquery方法因为重载原因有多个,args 就是指明参数类型,从而确定是哪一个方法。

2.2 Interceptor 接口

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    Object plugin(Object var1);
    void setProperties(Properties var1);
}

2.2.1 intercept方法

interceptor能够拦截的四种类型对象,此处入参invocation便是指拦截到的对象。

  Object intercept(Invocation var1) throws Throwable;

2.2.2 plugin方法

让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理

@Override
public Object plugin(Object target) {
   //判断是否拦截这个类型对象(根据 @Intercepts注解筛选),然后决定是返回一个代理对象还是返回原对象。
   //如果是插件要拦截的对象时才执行Plugin.wrap方法,否则的话,直接返回目标本身。
  return Plugin.wrap(target, this);
}

根据@Intercepts注解来决定是否进行拦截处理。

2.2.3 setProperties方法

拦截器需要一些变量对象,而且这个对象是支持可配置的。

2.3 原理

2.3.1 InterceptorChain拦截器链类

Mybatis中的InterceptorChain类用来创建拦截器链,内部持有一个interceptors的List,拦截器的顺序就是在配置文件中配置的拦截器的顺序

public class InterceptorChain {
  
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  
  //创建拦截器链,target为mybatis中的4大对象中的某一个(ParameterHandler,StatementHandler,ResultSetHandler,Executor)
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
     //通过jdk动态代理为目标创建代理对象,如果有多个拦截器,会出现代理对象再次被代理的情况,通过层层代理,构建拦截器链
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

pluginAll(target)方法是用来构建拦截器链的,以target对象是Executor为例,来看这个方法是在哪里被调用的:

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

每次调用newExecutor()方法时将目标对象executor传入interceptorChain.pluginAll(),返回executor的代理对象,其实在这个代理对象的内部,拦截器链已经形成了。

2.3.2 Plugin类

要想搞清楚拦截器链怎样构建的,必须需要深入interceptor.plugin(target)方法。

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

Plugin类实现了InvocationHandler接口,返回了一个JDK自身提供的动态代理类。

public class Plugin implements InvocationHandler {
  //目标对象,可能是一个代理对象,在第一次调用interceptor.plugin(target)时,target不是代理类,
  //之后调用interceptor.plugin(target)时,这里的target就是代理对象了
  private Object target;
  //拦截器对象,因为之后要在invoke方法里面调用拦截器的拦截方法,所以这里需要持有引用
  private Interceptor interceptor;
  //拦截器方法签名map,在invoke方法内部判断如果调用的是拦截器支持拦截的方法,否则,直接调用目标对象的方法
  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,//注意这里第三个参数创建了一个当前类对象,并将目标对象、拦截器对象和方法签名的map传入
          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)) {
        //注意方法参数,创建一个Invocation并将目标对象、方法和参数传进去,所以在Invocation对象内部可以通过反射调用目标对象的方法
        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);
    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()]);
  }

}

Plugin这个类的作用就是根据 @Interceptors注解,得到这个注解的属性 @Signature数组,然后根据每个 @Signature注解的type,method,args属性使用反射找到对应的Method。最终根据调用的target对象实现的接口决定是否返回一个代理对象替代原先的target对象。

比如MyBatis官网的例子

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class})
})

由于Executor接口的update(MappedStatement ms, Object parameter)方法被拦截器被截获。因此最终返回的是一个代理类Plugin,而不是Executor。调用方法的时候,那么会执行代理类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);
    }
}

Invocation的定义如下,它的proceed方法也就是调用原先方法(不走代理)。

public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  //调用原先方法(不走代理)
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

3 利用拦截器开发SQL延迟指标

接下来,利用Mybatis拦截器,我们开发一个SQL获取延迟指标,监控到的指标可以实时上报到监控平台,方便开发和运维关注应用的慢sql。

3.1 @Intercepts定义

Executorqueryupdate方法进行拦截::

@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                Object.class})
})

3.2 Interceptor实现

public class PerfInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement statement = (MappedStatement)invocation.getArgs()[0];
       // method:sql执行的 [Mapper接口]#[mapper方法]
        String method = this.parseMethod(statement);
        long start = System.currentTimeMillis();
        Object res = invocation.proceed();
        
        // latency:sql执行耗时
        long latency = System.currentTimeMillis() - start;
     
        // type:sql类型:如update, query
        String type = statement.getSqlCommandType().name();
        log.info("The latency of method {} is {}, type = {}", method, latency,  type);
        return res;
    }

    private String parseMethod(MappedStatement mappedStatement){
        String[] splits = StringUtils.split(mappedStatement.getId(), '.');
        return splits[splits.length - 2] + "#" + splits[splits.length - 1];
    }
}

3.3 加入Spring容器

第一步:在Spring中定义bean perfInterceptor
第二步:通过代码配置或者xml配置的方式将perfInterceptor配置到SqlSessionFactoryBean
下面是xml配置的示例:

    <!-- myBatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
         ...
        <property name="plugins">
            <list>
                <ref bean="perfInterceptor"/>
            </list>        
        </property>
    </bean>

3.4 完整代码

package com.artemis.xm.mybatis;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBoundsimport java.util.Properties;


@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                Object.class})
})
public class PerfInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement statement = (MappedStatement)invocation.getArgs()[0];

        // method:sql执行的 [Mapper接口]#[mapper方法]
        String method = this.parseMethod(statement);
        long start = System.currentTimeMillis();
        Object res = invocation.proceed();

        // latency:sql执行耗时
        long latency = System.currentTimeMillis() - start;

        // type:sql类型:如update, query
        String type = statement.getSqlCommandType().name();
        log.info("The latency of method {} is {}, type = {}", method, latency, type);
        return res;
    }

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

    public void setProperties(Properties properties) {
        // NOP
    }

    private String parseMethod(MappedStatement mappedStatement){
        String[] splits = StringUtils.split(mappedStatement.getId(), '.');
        return splits[splits.length - 2] + "#" + splits[splits.length - 1];
    }
}

参考文章