干翻Mybatis源码系列之第十篇:Mybatis拦截器基本开发、基本使用和基本细节分析

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 干翻Mybatis源码系列之第十篇:Mybatis拦截器基本开发、基本使用和基本细节分析

前言

Mybatis拦截器的开发基本上包含两个步骤:编码和配置。

拦截器编码当中需要实现拦截器的接口,在这个类上边基于注解标注我们需要拦截的目标。这就是自定义拦截器了。

一:拦截器接口说明

public interface Interceptor {
  //拦截前需要实现的功能+放行执行具体的Dao中的方法。
  Object intercept(Invocation invocation) throws Throwable;
  //这个方法的作用就是把这个拦截器的目标,传递给下一个拦截器。这种情况下适用于多个拦截器的存在
  //当第一个拦截器处理完毕之后,把处理完毕的目标,传递给下一个拦截器。
  //这个方法涉及的是目标传递的过程。
  Object plugin(Object target);
  //获取拦截器相关参数的
  void setProperties(Properties properties);
}

这里边真正起拦截作用的是intercept方法。

二:拦截器实现示意

1:拦截器编码

@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 MyMybatisInterceptor implements Interceptor {
    private String test;
    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor.class);
    @Override
    /**
     *  作用:执行的拦截功能 书写在这个方法中.
     *       放行
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isDebugEnabled())
            log.debug("----拦截器中的 intercept 方法执行------  "+test);
        return invocation.proceed();
    }
    /*
     *  把这个拦截器目标 传递给 下一个拦截器
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
    /*
     *  获取拦截器相关参数的
     */
    @Override
    public void setProperties(Properties properties) {
       this.test = properties.getProperty("test");
    }
}
@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})
})

这样一看就明白咋回事了,我们要拦截是的是Executor当中的方法,方法名字是query,args是方法中的参数,这个方法要严格和这个参数对应上。这样Mybatis就能唯一的确认要拦截哪个方法了

2:拦截器配置

后续我们要在Mybatis当中配置添加拦截器配置,通知Mybatis启动的时候加载当前开发的拦截器。

<plugins>
        <plugin interceptor="com.baizhiedu.plugins.MyMybatisInterceptor"/>
    </plugins>

这里边老铁们可能会有一个问题,咱们这个不是拦截的是Executor的query方法么,然后这里边真正走数据库查询的时候不是走的StatementHandler当中的方法么?这种理解是没有问题的,咱们拦截的是Executor当中的方法,但是Executor当中执行query的时候,底层走的也是StatementHandler当中的方法。拦住了Executor就相当于拦截住了StatementHandler当中的query。那同样拦截住了

三:拦截器作用示范

/**
     * 用于测试:Plugins的基本使用
     */
    @Test
    public void test1() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        UserDAO userDAO = session.getMapper(UserDAO.class);
        User user = userDAO.queryUserById(4);
        System.out.println("user = " + user);
        User newUser = new User(4, "xiaohuahua");
        userDAO.update(newUser);
        session.commit();
    }

执行结果如下:

2023-06-15 20:13:02 DEBUG LogFactory:135 - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2023-06-15 20:13:02 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 20:13:02 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 20:13:02 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 20:13:02 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 20:13:02 DEBUG MyMybatisInterceptor:28 - ----拦截器中的 intercept 方法执行------  111111
2023-06-15 20:13:02 DEBUG UserDAO:62 - Cache Hit Ratio [com.baizhiedu.dao.UserDAO]: 0.0
2023-06-15 20:13:02 DEBUG JdbcTransaction:137 - Opening JDBC Connection
2023-06-15 20:13:02 DEBUG PooledDataSource:406 - Created connection 1561408618.
2023-06-15 20:13:02 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5d11346a]
2023-06-15 20:13:02 DEBUG queryUserById:159 - ==>  Preparing: select id,name from t_user where id = ? 
2023-06-15 20:13:02 DEBUG queryUserById:159 - ==> Parameters: 4(Integer)
2023-06-15 20:13:02 DEBUG queryUserById:159 - <==      Total: 1
user = User{id=4, name='二姐'}
2023-06-15 20:13:02 DEBUG MyMybatisInterceptor:28 - ----拦截器中的 intercept 方法执行------  111111
2023-06-15 20:13:02 DEBUG update:159 - ==>  Preparing: update t_user set name=? where id=? 
2023-06-15 20:13:02 DEBUG update:159 - ==> Parameters: xiaohuahua(String), 4(Integer)
2023-06-15 20:13:02 DEBUG update:159 - <==    Updates: 1
2023-06-15 20:13:02 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5d11346a]
Process finished with exit code 0

我们开发完毕拦截器之后,Mybatis启动的时候自动为我们进行加载,运行的时候自动走拦截器。

四:拦截器作用解析

1:如何给拦截器注入参数?

首先需要在Mybatis-config.xml当中通过property标签配置拦截器属性

<plugins>
        <plugin interceptor="com.baizhiedu.plugins.MyMybatisInterceptor">
            <property name="test" value="111111"/>
        </plugin>
    </plugins>

然后在MyMybatisInterceptor拦截器初始化的时候,基于其中setProperties方法进行拦截器属性赋值,赋值之后在跑动的时候就可以在使用这些属性了。

@Override
    public void setProperties(Properties properties) {
       this.test = properties.getProperty("test");
    }

上边日志中的:

2023-06-15 20:13:02 DEBUG MyMybatisInterceptor:28 - ----拦截器中的 intercept 方法执行------  111111

不就是最好的说明么?

2:如何配置拦截多个方法?

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
@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})
})

@Intercepts({})这里边的{}就代表了数组,如果只有一个数据的话,{}是可以去掉的。

五:拦截器细节分析

1:拦截器想要拦截SQL,如何拦截最合适?

Executor的功能是比较繁杂的,有增删改查包括事务的一些操作,而他真正增删改查的操作是交给StatementHandler来做,StatementHandler当中的操作就比较单一了,所以我们把拦截放到StatementHandler上是比较合理的。statementHandler当中只有两个query和一个update,然后里边还有一个prepare方法(BaseStatementHandler当中写的,完成Mybatis当中所有的Statement对象的创建),这个方法的作用是准备Statement给StatementHandler中的query和update使用。

Ps:StatementHandler使用的是装饰器设计模式,然后也有适配器设计模式。

所以,如果拦截器的目的是获取SQL的话,最合适的方法就是拦截BaseStatementHandler当中的prepare这个唯一生产Statement对象的方法。

而且,我们观察下prepare这个方法:

@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

这里边我们看到这个方法里边有Connection,有了连接对象之后,我们就可以拿到所有的JDBC中的对象。

2:拦截器想要拦截SQL,为什么这么合适?

1:BaseStatementHandler中的prepare方法生产所有的Statement对象。

2:prepare方法里边有Connection,有了连接对象之后,我们就可以拿到所有的JDBC中的对象

3:拦截器拦截prepare方法测试

public abstract class MyMybatisInterceptorAdapter implements Interceptor {
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
}
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyMybatisInterceptor2 extends MyMybatisInterceptorAdapter {
    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor2.class);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isDebugEnabled())
            log.debug("----拦截器中的 MyMybatisInterceptor2   intercept方法执行------  "+test);
        return invocation.proceed();
    }
    @Override
    public void setProperties(Properties properties) {
    }
}
<plugins>
        <plugin interceptor="com.baizhiedu.plugins.MyMybatisInterceptor2"/>
    </plugins>

执行结果如下:

2023-06-15 21:16:03 DEBUG LogFactory:135 - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2023-06-15 21:16:03 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:16:03 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:16:03 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:16:03 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:16:04 DEBUG UserDAO:62 - Cache Hit Ratio [com.baizhiedu.dao.UserDAO]: 0.0
2023-06-15 21:16:04 DEBUG JdbcTransaction:137 - Opening JDBC Connection
2023-06-15 21:16:04 DEBUG PooledDataSource:406 - Created connection 929776179.
2023-06-15 21:16:04 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@376b4233]
2023-06-15 21:16:04 DEBUG MyMybatisInterceptor2:25 - ----拦截器中的 MyMybatisInterceptor2   intercept方法执行------  
2023-06-15 21:16:04 DEBUG queryUserById:159 - ==>  Preparing: select id,name from t_user where id = ? 
2023-06-15 21:16:04 DEBUG queryUserById:159 - ==> Parameters: 4(Integer)
2023-06-15 21:16:04 DEBUG queryUserById:159 - <==      Total: 1
Process finished with exit code 0

4:如何拦截器中获取SQL

我们先找到boundSql对象

具体的代码如下,有两种实现方式:

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        //RoutingStatementHandler---delegate---
        //RoutingStatementHandler satementHandler = (RoutingStatementHandler) invocation.getTarget();
        //BoundSql boundSql = satementHandler.getBoundSql();
        //String sql = boundSql.getSql();
        //log.info("sql:",sql);
    基于Mybatis提供的反射工厂来干。直接打破封装用反射即可,以下是Mybatis低等用于反射的对象。
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        if (log.isDebugEnabled()) {
            log.debug("sql : " + sql);
        }
        return invocation.proceed();
    }

具体的实现结果如下:

Connected to the target VM, address: '127.0.0.1:34056', transport: 'socket'
2023-06-15 21:26:00 DEBUG LogFactory:135 - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2023-06-15 21:26:00 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:26:00 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:26:00 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:26:00 DEBUG PooledDataSource:335 - PooledDataSource forcefully closed/removed all connections.
2023-06-15 21:38:58 DEBUG UserDAO:62 - Cache Hit Ratio [com.baizhiedu.dao.UserDAO]: 0.0
2023-06-15 21:38:58 DEBUG JdbcTransaction:137 - Opening JDBC Connection
2023-06-15 21:38:58 DEBUG PooledDataSource:406 - Created connection 1906879951.
2023-06-15 21:38:58 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@71a8adcf]
2023-06-15 21:38:58 DEBUG MyMybatisInterceptor2:25 - ----拦截器中的 MyMybatisInterceptor2   intercept方法执行------  
2023-06-15 21:38:58 DEBUG queryUserById:159 - ==>  Preparing: select id,name from t_user where id = ? 
2023-06-15 21:38:58 DEBUG queryUserById:159 - ==> Parameters: 4(Integer)
2023-06-15 21:38:58 DEBUG queryUserById:159 - <==      Total: 1
Disconnected from the target VM, address: '127.0.0.1:34056', transport: 'socket'
Process finished with exit code 0

5:细节说明

MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        if (log.isDebugEnabled()) {
            log.debug("sql : " + sql);
        }

Mybatis当中很多反射操作都是这么干的,这样写更加Mybatis一点,这样操作是基于对象从属的层级一层一层点进去的,当然如果我们想要去给他这样赋值也是可以的。

String sql = (String) metaObject.set("target.delegate.boundSql.sql","select * from user where id = ?");
相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
安全 Java 应用服务中间件
阿里技术官架构使用总结:Spring+MyBatis源码+Tomcat架构解析等
分享Java技术文以及学习经验也有一段时间了,实际上作为程序员,我们都清楚学习的重要性,毕竟时代在发展,互联网之下,稍有一些落后可能就会被淘汰掉,因此我们需要不断去审视自己,通过学习来让自己得到相应的提升。
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——实体层(User.java)
mybatis简单案例源码详细【注释全面】——实体层(User.java)
13 0
|
2天前
|
Java 关系型数据库 数据库连接
MyBatis 执行流程分析
MyBatis 执行流程分析
|
15天前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
28 1
深入源码:解密MyBatis数据源设计的精妙机制
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
13 0
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
mybatis简单案例源码详细【注释全面】——测试层(UserMapperTest.java)
9 0
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
mybatis简单案例源码详细【注释全面】——Dao层映射文件(UserMapper.xml)【重要】
10 0
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Dao层接口(UserMapper.java)
mybatis简单案例源码详细【注释全面】——Dao层接口(UserMapper.java)
7 0
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——实体层(Role.java)
mybatis简单案例源码详细【注释全面】——实体层(Role.java)
7 0