MyBatis 拦截器使用及原理

MyBatis 拦截器介绍

MyBatis 提供了一种插件(即 MyBatis 拦截器)机制,可以拦截到 MyBatis 执行流程中的某些操作,从而实现一些特殊的功能。常见的应用场景比如分页、权限控制、日志打印、性能监控等。

MyBatis 拦截器使用

大致分两步:

  1. 创建拦截器类:实现Interceptor接口,重写intercept方法(在此方法中定义拦截逻辑);
  2. 配置拦截器:在 MyBatis 配置文件中注册拦截器。

1. 创建拦截器类

首先我们看下 MyBatis 拦截器的接口定义:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

intercept 方法是核心方法,用于拦截目标对象的方法调用,返回值是目标方法的执行结果。

下面是一个拦截器示例,该拦截器会拦截 Executor 接口的update(MappedStatement ms, Object parameter)方法(包括新增,删除,修改操作),所有对该方法的调用都会被该拦截器拦截到。

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.mapping.MappedStatement;

import java.util.Properties;

@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 这里可以添加拦截逻辑,比如日志记录
        System.out.println("Before executing: " + invocation.getMethod().getName());
        
        // 执行目标方法
        Object result = invocation.proceed();
        
        // 可以在这里添加后处理逻辑
        System.out.println("After executing: " + invocation.getMethod().getName());
        
        return result;
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}

注意拦截器实现了 Interceptor 接口,intercept 方法中包含拦截逻辑。@Signature 和 @Signature 注解用于标识该拦截器所需要拦截的方法。

2. 配置拦截器

在 MyBatis 的配置文件(如mybatis-config.xml)中注册拦截器:

<configuration>
    <plugins>
        <plugin interceptor="com.example.MyInterceptor"></plugin>
    </plugins>
</configuration>

MyBatis 拦截器原理

拦截器的注册

首先我们看下注册拦截器的代码,在 XMLConfigBuilder 中,pluginElement 方法将会读取到配置文件中配置的拦截器:

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 方法,将 interceptorInstance 添加到 interceptorChain 中:

public void addInterceptor(Interceptor interceptor) {
  interceptorChain.addInterceptor(interceptor);
}

interceptorChain 是 Configuration 的内部属性,类型为 InterceptorChain,InterceptorChain 类的 addInterceptor 方法将拦截器保存到列表中:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

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

}

拦截器拦截目标对象

现在知道拦截器是如何注册到 MyBatis 中的,接下来我们看下拦截器是如何拦截目标方法的。

在 Configuration 的 newExecutor 方法中,会调用 InterceptorChain 的 pluginAll 方法,将拦截器应用于目标对象上:

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

拦截器可用于拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 的方法,因为每种类型的创建方法都调用了 pluginAll 方法,将拦截器应用于目标对象上:

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

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

pluginAll 方法会遍历拦截器列表,依次调用拦截器的 plugin 方法,将目标对象包装成代理对象,并返回:

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

所以 Interceptor 的 plugin 方法中,通常会基于动态代理对目标对象进行代理,拦截目标对象的方法调用,并进行相应的处理。

创建动态代理对象可以使用 JDK 的 Proxy 类的 newProxyInstance 方法:

public Object plugin(Object target) {
  return Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new MyInvocationHandler(target));
}

MyInvocationHandler 类需要实现 java.lang.reflect.InvocationHandler 接口的 invoke 方法,在方法内进行相应的代理逻辑处理:

class MyInvocationHandler implements InvocationHandler {

  private final Object target;

  public MyInvocationHandler(Object target) {
    this.target = target;
  }

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before method " + method.getName());
    Object result = method.invoke(target, args);
    System.out.println("after method " + method.getName());
    return result;
  }

}

@Intercepts、@Signature 和 Plugin 类

因为拦截器会应用到 Executor、ParameterHandler、ResultSetHandler、StatementHandler 各个类型的各个方法上,所以实际实现时需要判断目标对象的类型及当前被调用的方法,并进行相应的处理。为简化这部分过程,MyBatis 提供了 @Intercepts、@Signature 和 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;
}

可以看到 Plugin 类的 wrap 方法会先获取拦截器的拦截签名信息(也就是 @Intercepts 和 @Signature 标注出拦截哪些接口的哪些方法),然后根据签名信息判断目标对象是否需要被代理(也就是是否实现了拦截器所需要拦截的接口),如果需要被代理,则使用 JDK 的 Proxy 类的 newProxyInstance 方法创建动态代理对象,并返回。Plugin 类本身实现了 InvocationHandler 接口,在拦截方法时,会调用到其实现的 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);
  }
}

在 invoke 方法中,会判断当前被调用的方法是否被拦截器所拦截,如果是,则调用拦截器的 intercept 方法,否则直接调用目标对象的方法。调用 intercept 方法时,会封装当前被调用的方法相关信息,并作为参数传递给拦截器的 intercept 方法,这样拦截器就可以根据相关信息进行相应的处理。

所以,Plugin 类的作用是创建动态代理对象,拦截目标对象的方法调用,调用拦截器的 intercept 方法进行方法拦截。它使得拦截器实现类可以专注于实现拦截逻辑,而无需关心代理对象的创建和调用。

整个过程就是:

  1. Configuration 的 newExecutor 等方法会调用 InterceptorChain 的 pluginAll 方法
  2. InterceptorChain 的 pluginAll 方法会遍历拦截器列表,依次调用拦截器的 plugin 方法
  3. 拦截器的 plugin 方法会调用 Plugin 类的 wrap 方法(假设拦截器都通过该方法创建动态代理对象)
  4. Plugin 类的 wrap 方法会判断目标对象是否需要被代理,如果需要被代理,则使用 JDK 的 Proxy 类的 newProxyInstance 方法创建动态代理对象,并返回
  5. 结果就是被代理的对象被层层代理,当调用方法时,会依次调用 Plugin 类的 invoke 方法,invoke 方法会调用拦截器的 intercept 方法,继而拦截器完成相应的处理

setProperties 方法

setProperties 方法用于设置拦截器的属性,比如有如下拦截器:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;

import java.util.Properties;

@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
public class TimingInterceptor implements Interceptor {
    private long threshold; // 记录阈值

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // 执行目标方法
        Object result = invocation.proceed();
        
        long executionTime = System.currentTimeMillis() - startTime;
        if (executionTime > threshold) {
            System.out.println("SQL执行时间超过阈值: " + executionTime + "ms");
        }
        
        return result;
    }

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

    @Override
    public void setProperties(Properties properties) {
        // 从属性中获取阈值
        String thresholdStr = properties.getProperty("threshold");
        if (thresholdStr != null) {
            threshold = Long.parseLong(thresholdStr);
        }
    }
}

在 MyBatis 配置文件中注册拦截器时,可以设置拦截器的属性:

<configuration>
    <plugins>
        <plugin interceptor="com.example.TimingInterceptor">
            <property name="threshold" value="1000"/> <!-- 设置阈值为1000毫秒 -->
        </plugin>
    </plugins>
</configuration>

原理就是前面的 pluginElement 方法,会读取 properties,传给拦截器的 setProperties 方法:

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

参考:MyBatis 拦截器原理探究、ChatGPT

posted @ 2024-12-14 00:55  Higurashi-kagome  阅读(22)  评论(0编辑  收藏  举报