搞懂MyBatis拦截器的工作原理

在日常开发过程中,我们经常会碰到这样一种场景,在对某一个请求的处理过程中添加一定的特殊逻辑,但又不想对整个处理流程中的其他步骤造成影响。比如,我们希望在两个业务操作之间嵌入一个安全控制机制。

请求处理流程中嵌入定制化操作的示意图

显然,想要实现这种效果的方式有很多种。今天,我们就来介绍一种非常实用的实现方法,也就是拦截器(Interceptor)。现在,让我们从拦截器的设计思想开始讲起。

拦截器的设计思想

对于拦截过程而言,我们首先要明确的是它的拦截点。在拦截器运行过程中,拦截点表示应用执行过程中能够插入拦截器的一个点。这种拦截点可以是普通的方法调用、类初始化或对象实例化,也可以是针对异常的处理。

一旦捕获了拦截点,我们就可以通过反射机制获取这个拦截点对应的执行方法、输入参数、目标返回值等元数据,从而根据这些元数据来实现一系列自定义拦截操作。

拦截点结构图

最后,将拦截点和拦截操作结合在一起就构成了拦截器。本质上,拦截器用于定义应用程序中的业务逻辑及其执行的位置。我们可以通过一张图来展示拦截器的组成结构。

拦截器组成结构示意图

拦截器的设计思想非常通用,所以它在各大主流开源框架中的应用也非常广泛。今天,我们就以常见的 ORM 框架------MyBatis 为例,详细分析一下 ORM 框架中所具备的拦截器的工作原理。

MyBatis 中的拦截器工作原理

MyBatis 中内置了一组常用的拦截器,而开发人员也可以通过 Plugin 配置项,来嵌入各种定制化的拦截器。我们先来看一下在 MyBatis 中使用拦截器的方式。通常,就是在配置文件中添加类似如下所示的配置项。可以看到,在 Plugin 配置段中可以添加一个自定义的 interceptor 配置项,并设置对应的属性。

xml 复制代码
<plugins>
     <plugin interceptor="com.xiaoyiran.Mybatis.interceptor.MyInterceptor">
          <proper  ty name="prop1″ value="prop1″/>
          <property name="prop2" value="prop2"/>
    </plugin>
</plugins>

MyBatis 中的 Configuration 类会根据配置的拦截器属性,实例化 Interceptor 对象,并添加到 MyBatis 的运行上下文中。我们跟踪代码,发现 Configuration 中定义了一个 InterceptorChain 对象。显然,所有的 Interceptor 实例最终会被添加到这个 InterceptorChain 中。

java 复制代码
protected final InterceptorChain interceptorChain = new InterceptorChain();

这样,MyBatis 中,代表拦截器的 Interceptor 和代表拦截器链的 InterceptorChain 的这两个核心对象都出现了。其中 Interceptor 是个接口,InterceptorChain 是个实体类,它们的代码看上去都不多。让我们先来看一下 InterceptorChain 类:

typescript 复制代码
public class InterceptorChain {
privatefinal 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 提供了 addInterceptor 方法,用于将拦截器添加到链中。这个类持有一个 interceptors 数组,用于把新加入的 Interceptor 保存起来。通过这种实现方式,在 pluginAll 方法中,我们就可以直接遍历 interceptors 数组,并利用每个 interceptor 执行拦截逻辑。

这里我们不明确的就是 interceptor.plugin(target) 方法的逻辑,让我们把思路回到 Interceptor 接口。

typescript 复制代码
public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  default void setProperties(Properties properties) {
    // NOP
  }
}

可以看到,Interceptor 接口中的 plugin 方法实际上存在一个默认实现,这里它通过 Plugin.wrap 方法完成了对目标对象的拦截。Plugin.wrap 是一个静态方法,实现过程如下所示:

typescript 复制代码
public class Plugin implements InvocationHandler {
//省略变量定义和构造函数
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;
  }
  ...
}

这里我们看到了熟悉的 InvocationHandler 接口和 Proxy.newProxyInstance 实现方法,从而明白了,原来这里用到了 JDK 的动态代理机制。我们通过 getSignatureMap 方法从拦截器的注解中获取拦截的类名和方法信息,然后,通过 getAllInterfaces 方法获取接口。最后,通过动态代理机制产生代理。这样使得只有是 Interceptor 注解的接口实现类才会产生代理。

讲完 Interceptor 和 InterceptorChain 之后,让我们再次回到 Configuration 类,并找到以下代码:

ini 复制代码
public ParameterHandler newParameterHandler(...) {
    ParameterHandler parameterHandler = ...;
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}
public ResultSetHandler newResultSetHandler(...) {
    ResultSetHandler resultSetHandler = ...;
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}
public StatementHandler newStatementHandler(...) {
    StatementHandler statementHandler = ...;
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    Executor executor  = ...;
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

讲解这段代码的目的是在说明这样一个事实:在 MyBatis 中,拦截器只能拦截 ParameterHandler、StatementHandler、ResultSetHandler 和 Executor 这四种类型的接口,这点在 Configuration 类中是通过以上代码预先定义好的。这些就构成了 MyBatis 中针对拦截器的各个拦截点。如果我们想要实现自定义拦截器,也只能围绕上述四种接口添加逻辑。这四个接口之间的关系和拦截顺序如下图所示:

MyBatis 中能够累计额的四种接口类型及其顺序

对于 SQL 的执行过程而言,这四个环节的拦截机制基本可以满足日常的定制化需求了。

自定义 MyBatis 拦截器的实现方法

虽然 MyBatis 已经内置了一组强大的拦截器,我们可以基于这组拦截器来应对常见需求。但针对某些特定的应用场景,有时候我们就需要自己来实现定制化的拦截器。接下来,我们就来看一下如何在 MyBatis 中自定义一个 Interceptor。

如果想要在 MyBatis 中实现一个自定义拦截器,我们要做的事情就是实现上面介绍的 Interceptor 接口,并在这个接口上指定相应的 Signature 信息。一个空白的 Interceptor 实现类模版如下所示:

typescript 复制代码
@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        returnPlugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

在上面这个 MyInterceptor 类的 intercept 方法中,我们需要调用 invocation.proceed() 方法来完成 InterceptChain 的执行流程,而我们可以在这个方法的前后添加定制化处理过程。

然后,我们来考虑一个拦截器的常见应用场景。

在实现数据库插入和更新操作时,我们往往需要对这条记录的更新时间进行同步更新。我们当然可以为每句 SQL 添加相应的时间处理方法,但更好的一种方式是通过自定义拦截器的方式来自动完成这一步操作。

显然,这一步操作应该是在 Executor 中进行完成。根据上面对 Plugin 类的介绍,我们首先需要明确 Executor 中需要拦截的方法,而这方法就是如下所示的 update 方法:

sql 复制代码
int update(MappedStatement ms, Object parameter) throws SQLException;

明确了 Signature 信息之后,我们就可以来着手实现整个流程了。为了方便起见,我们可以提供一个如下所示的注解,专门用来标识具体需要进行拦截的字段:

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface UpdateTimeStamp {
   String value() default "";
}

然后,我们创建一个业务领域类,把该注解作用于具体的更新时间字段上。

kotlin 复制代码
public class MyDomain {
 //省略其他字段定义
 @UpdateTimeStamp
 public D  ate updateTimeStamp;
}

完整的 UpdateTimeStampInterceptor 实现如下,我们对关键代码都添加了注释:

scss 复制代码
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateTimeStampInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
     //获取 MappedStatement
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        //获取 SqlCommandType
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        //获取 Parameter
        Object parameter = invocation.getArgs()[1];
        if (parameter != null) {
            Field[] declaredFields = parameter.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
             //获取 UpdateTimeStamp 注解
                if (field.getAnnotation(UpdateTimeStamp.class) != null) {
                 //如果是 Insert 或 Update 操作,则更新操作时间
                    if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
                        field.setAccessible(true);
                        if (field.get(parameter) == null) {
                         //设置参数值
                            field.set(parameter, new Date());
                        }
                    }
                }
            }
        }
        //继续执行拦截器链
        return invocation.proceed();
    }
  ...
}

上面这个 UpdateTimeStampInterceptor 类的实现过程,展示了如何获取与 Executor 相关的 Statement、SQL 类型以及所携带的参数。通过这种方法,我们可以实现在新增或者删除数据库记录时,动态地添加所需要的字段值。同样,这种处理方式可以扩展到任何我们想要处理的字段和参数。

总结

从本质上讲,MyBatis 中实现拦截的基本手段是构建了一个拦截器链,这和设计模式中的责任链模式比较类似。而在底层原理上,拦截操作的实现还是基于动态代理机制,通过获取对应方法的签名、输入的接口和参数等信息来生成代理,从而确保我们可以在代理对象中添加各种自定义的拦截逻辑。

基于 MyBatis 中的拦截器机制,还针对 Executor 的 update 方法给出了一个自定义的 Interceptor 实现,用于动态设置数据库中某些数据项的值。这些做法都可以直接应用到日常开发过程中。

相关推荐
xuejianxinokok17 分钟前
解惑rust中的 Send/Sync(译)
后端·rust
Siler27 分钟前
Oracle利用数据泵进行数据迁移
后端
mjy_11133 分钟前
Linux下的软件编程——文件IO
java·linux·运维
用户67570498850237 分钟前
3分钟,手摸手教你用OpenResty搭建高性能隧道代理(附完整配置!)
后端
进阶的小名41 分钟前
@RequestMapping接收文件格式的形参(方法参数)
java·spring boot·postman
coding随想1 小时前
网络世界的“快递站”:深入浅出OSI七层模型
后端·网络协议
skeletron20111 小时前
🚀AI评测这么玩(2)——使用开源评测引擎eval-engine实现问答相似度评估
前端·后端
shark_chili1 小时前
颠覆认知!这才是synchronized最硬核的打开方式
后端
就是帅我不改1 小时前
99%的Java程序员都写错了!高并发下你的Service层正在拖垮整个系统!
后端·架构
Apifox1 小时前
API 文档中有多种参数结构怎么办?Apifox 里用 oneOf/anyOf/allOf 这样写
前端·后端·测试