深入浅出Mybatis拦截器

简介: 我们平时所谈的拦截器与过滤器有什么区别?我们在使用Mybatis时候,如果想动态的改写sql如何实现?倘若在多租户的系统中,如何依据当前的线程上下文中的请求租户信息,动态的改写sq设置租户信息?又或者如何增加sql的执行耗时或者信息摘要呢?

image.png

前言

在正式使用拦截器之前,首先要抛出几个问题,我们平时所谈的拦截器与过滤器有什么区别?我们在使用Mybatis时候,如果想动态的改写sql如何实现?倘若在多租户的系统中,如何依据当前的线程上下文中的请求租户信息,动态的改写sq设置租户信息?又或者如何增加sql的执行耗时或者信息摘要呢?

拦截器与过滤器

  • 过滤器(Filter):见名知意我们使用过滤器的目的,无非就是用来做过滤操作的。比如对请求前置过滤掉一些信息,或者前置设置某些参数,然后在从Controller根据这些业务特征进行对应的逻辑操作。通常用的场景是:ip白名单、用户信息的过滤、敏感信息的过滤等等。
  • 拦截器(Interceptor):从名称看来它类似于AOP实现了某些拦截操作,其就是在某些方法前/后,执行某些特殊的业务逻辑。而动态代理就是拦截器的简单实现,在调用方法前/后做其它业务逻辑的操作,也可以在抛出异常的时候做某些特殊的业务逻辑操作。

Mybatis拦截器使用姿势

先不谈Mybatis如何实现的拦截器加载原理,我们直接上手去配置拦截器。但是为了方便下个小节查看源码,这里使用原生的拦截器配置方式,更多的配置方式,可以去查下相关资料。

首先配置拦截器分为以下几步
  1. 继承org.apache.ibatis.plugin.Interceptor接口、配置@Intercepts与@Signature组合注解。(详情见下个小节)
/**
* @author Duansg
* @date 2022-10-08 11:45 下午
*/
@Intercepts(value = {
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}
           )
    public class ExampleInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.err.println("晓断测试Interceptor");
            return invocation.proceed();
        }

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

        @Override
        public void setProperties(Properties properties) {
            System.err.println("晓断测试Interceptor-Properties" + JSON.toJSONString(properties));
        }
    }
  1. 在mybatis-config.xml中配置拦截器
<?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.xxx.xxx.xxx.ExampleInterceptor">
            <property name="someProperty" value="100"/>
        </plugin>
    </plugins>
    .....
</configuration>
  1. 验证拦截器是否生效
00:05:51.099 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
晓断测试Interceptor-Properties{"someProperty":"100"}
00:05:51.231 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
晓断测试Interceptor

Mybatis拦截器@Intercepts与@Signature

在谈这些配置之前,可以翻阅一下笔者发布的Mybatis核心组件的文章,以下为简要信息。在下图中,Mybatis拦截器的拦截点有4种,分别为Executor(执行器)、StatementHandler(sql语法构建处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果处理器)。
image.png

  • @Intercepts:它用于标识当前的对象是一个拦截器,其配置值是一个@Signature的集合。
  • @Signature:它用于标识需要拦截的接口、方法、对应的参数列表。

Mybatis拦截器实现原理

上个小节提到Mybatis拦截器的拦截点有4种,但是他们是如何加载拦截并执行的呢?直接上代码

// 大家经常见到的mybatis的用法
public static void main(String[] args) throws IOException {
        SqlSession session = null;
        try {
            // ①:加载Mybatis配置文件
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // ②:构建SqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // ③:获取SqlSession并获取代理对象查询
            session = sqlSessionFactory.openSession();
            ProductDao mapper = session.getMapper(ProductDao.class);
            System.out.println(mapper.findById(100001L));
        } finally {
            if (!ObjectUtils.isEmpty(session)) {
                session.close();
            }
        }
    }
/**
 * @author Duansg
 * @date 2022-10-08 11:45 下午
 */
@Intercepts(value = {
    @Signature(type = Executor.class, method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    @Signature(type = Executor.class, method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}
)
public class ExampleInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        System.err.println("晓断测试Interceptor");
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
        System.err.println("晓断测试Interceptor-Properties" + JSON.toJSONString(properties));
    }
}
<!--mybatis-config.xml -->
<?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="cn.**.**.test.ExampleInterceptor"/>
            <property name="someProperty" value="100"/>
    </plugin>
  </plugins>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="****"/>
        <property name="username" value="****"/>
        <property name="password" value="****"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="ProductsMapper.xml"/>
  </mappers>
</configuration>
源码解析

通过此上代码,①跟②这一步没什么问题,前者就是直接根据配置文件获取输入流,后者获取

// new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // ①:XMLConfigBuilder它主要负责解析mybatis-config.xml配置文件,它会初始化configuration对象。
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // ②:parse()方法是解析mybatis-config.xml配置文件的入口。详见下一步
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}
// parser.parse();
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // ①:具体的解析过程,对应的就是mybatis-config.xml中的整个configuration节点,详见下一步
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
// parseConfiguration(parser.evalNode("/configuration"));
// 看到这个方法你可能就熟悉了,这里解析的配置不都是我们在configuration中配置的节点么。
// 本期不讨论其他配置的加载,直接看plugins节点的解析
  private void parseConfiguration(XNode root) {
    try {
      Properties settings = settingsAsPropertiess(root.evalNode("settings"));
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      // ①:拦截器加载。详见下一步
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectionFactoryElement(root.evalNode("reflectionFactory"));
      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);
    }
  }
// pluginElement(root.evalNode("plugins"));
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      // 这个parent就是plugins整个节点,getChildren就是获取它的子节点plugin节点
      for (XNode child : parent.getChildren()) {
        // ①:获取plugin中的interceptor属性,
        String interceptor = child.getStringAttribute("interceptor");
        // ②:解析property节点的name、value
        Properties properties = child.getChildrenAsProperties();
        // ③:实例化配置的Interceptor
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // ④:设置自定义的属性配置
        interceptorInstance.setProperties(properties);
        // ⑤※:添加到configuration中,详见下一步。
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
// interceptorChain就是拦截器链,它在configuration初始化创建,其内部维护这一个空的list类型的Interceptor集合
public void addInterceptor(Interceptor interceptor) {
    // ①:添加到拦截器链中,详见下一步。
    interceptorChain.addInterceptor(interceptor);
}
/**
 * @author Clinton Begin
 */
public class InterceptorChain {

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

  /* ②:加载所有拦截器方法,它会在创建Executor、StatementHandler、
    ParameterHandler、ResultSetHandler时加载。可以去这些拦截点创建的地方查看。
    例如Executor创建:org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
  */
  // 需要注意的是,这个target就是那4个拦截点的对象,不过多赘述。
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // ③:调用每个拦截器的plugin方法,用于封装拦截点对象的,通过该方法返回一个拦截点的代理对象。详见下一步
      // 这个plugin方法就是我们在实现拦截器的时候返回的return Plugin.wrap(target, this);
      target = interceptor.plugin(target);
    }
    return target;
  }

  // ①:书接上回,就是将这个拦截器添加到集合中
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

}
@Override
public Object plugin(Object target) {
   // ①:生成拦截点的代理对象,详见下一步。
   return Plugin.wrap(target, this);
}
public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  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) {
    // ①:解析我们拦截器配置的@Intercepts跟@Signature注解配置。
    // k:@Signature配置的type具体拦截点类型,
    // v:@Signature配置的method具体的拦截方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // ②:获取拦截点的class
    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)) {
        // ⑦:如果有,执行我们自定义拦截器中的intercept方法。
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
  // .....
}

通过此上分析,我们在执行Executor、StatementHandler、ParameterHandler、ResultSetHandler时的方法的时候,其实调用的都是代理对象,而代理对象通过方法增强的方式,在调用方法执行前判断是否需要拦截,并执行相关的自定义拦截逻辑的。

Mybatis拦截点加载顺序

不知道客官有没有考虑过Mybatis这4种拦截点的加载顺序?它一定是按照组件的调用顺序拦截的吗?答案是不一定的,网上很多有说的顺序是Executor -> ParameterHandler -> ResultHandler -> StatementHandler,其实这也不全对。

拦截点顺序验证

代码就不展示了,没什么营养,就是配置了4种拦截点的拦截器。验证下来,结论就是:Executor首先执行的是没有问题的。ParameterHandler -> ResultHandler 的先后顺序也没有问题,一进一出嘛。问题就是StatementHandler的顺序。

  • 如果你拦截的是StatementHandler.query()方法,那么顺序是Executor -> ParameterHandler -> StatementHandler -> ResultHandler。
  • 如果你拦截的是StatementHandler.prepare()方法,那么顺序是Executor -> StatementHandler-> ParameterHandler -> ResultHandler。
源码解析
// 我们在拦截Executor的query方法的时候,最终都会走到doQuery的方法逻辑中,
// Executor拦截query肯定首先执行没有问题,
 @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // ①:关键在于这个预编译的过程中,详见下一步。
      stmt = prepareStatement(handler, ms.getStatementLog());
      // ②:此处就是拦截的StatementHandler.query()方法。记住这里
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // ①:这个就是拦截的StatementHandler.prepare()方法。
    stmt = handler.prepare(connection, transaction.getTimeout());
    // ②:这个底层就是调用的ParameterHandler中的setParameters方法。
    handler.parameterize(stmt);
    return stmt;
  }

从上面两段代码中不难看出,如果是拦截的StatementHandler.prepare()方法,那么它的执行时机是在ParameterHandler执行之前的,所有StatementHandler会先执行拦截。相反,如果是StatementHandler.query()方法,它要等预编译之后在进行查询,所以到这里就能说明"也不全对"的言论了。

Mybatis拦截器应用场景

  • Sql摘要统计/监控
  • 动态分页
  • 多租户
  • 脱敏
  • 加解密
  • 冗余字段新增
  • .......等等

全剧终

通过本篇文章,相信你肯定会对Mybatis的拦截器有一个充分的认知,但文中屏蔽掉了许多细节,这些细节还需要你根据源码依照文中对应的注释去串一下自己的逻辑,也欢迎你指出文章中的错误信息,以便笔者及时改正,以免误人子弟,因排版问题,可能阅读起来不是很通顺,也欢迎来指正说明。

目录
相关文章
|
4天前
|
SQL 监控 Java
mybatis拦截器实现
mybatis拦截器实现
14 0
|
4天前
|
SQL Java 数据库连接
Mybatis拦截器实现带参数SQL语句打印
Mybatis拦截器实现带参数SQL语句打印
|
4天前
|
SQL Java 数据库连接
MyBatis源码篇:mybatis拦截器源码分析
MyBatis源码篇:mybatis拦截器源码分析
|
4天前
|
存储 SQL Java
干翻Mybatis源码系列之第十二篇:自写Mybatis拦截器实现分页操作
干翻Mybatis源码系列之第十二篇:自写Mybatis拦截器实现分页操作
|
4天前
|
SQL Java 数据库连接
干翻Mybatis源码系列之第十一篇:Mybatis拦截器获取被拦截对象的方法和参数
干翻Mybatis源码系列之第十一篇:Mybatis拦截器获取被拦截对象的方法和参数
|
4天前
|
SQL 设计模式 Java
干翻Mybatis源码系列之第十篇:Mybatis拦截器基本开发、基本使用和基本细节分析
干翻Mybatis源码系列之第十篇:Mybatis拦截器基本开发、基本使用和基本细节分析
|
4天前
|
设计模式 Java 数据库连接
学会自己编写Mybatis插件(拦截器)实现自定义需求2
学会自己编写Mybatis插件(拦截器)实现自定义需求
72 0
|
4天前
|
XML Java 数据库连接
学会自己编写Mybatis插件(拦截器)实现自定义需求1
学会自己编写Mybatis插件(拦截器)实现自定义需求
92 0
|
4天前
|
安全 前端开发 Java
Java反射详解,学以致用,实战案例(AOP修改参数、Mybatis拦截器实现自动填充)3
Java反射详解,学以致用,实战案例(AOP修改参数、Mybatis拦截器实现自动填充)
83 0
|
4天前
|
Java 数据库连接 API
Java反射详解,学以致用,实战案例(AOP修改参数、Mybatis拦截器实现自动填充)2
Java反射详解,学以致用,实战案例(AOP修改参数、Mybatis拦截器实现自动填充)
44 0