给自己的每日一句

不从恶人的计谋,不站罪人的道路,不坐亵慢人的座位,惟喜爱耶和华的律法,昼夜思想,这人便为有福!他要像一棵树栽在溪水旁,按时候结果子,叶子也不枯干。凡他所做的尽都顺利。

本文内容整理自《孙哥说Mybatis系列视频课程》,老师实力十分雄厚,B站搜孙帅可以找到本人

前言

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,如何拦截最合适?

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

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

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

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

所以,如果拦截器的目的是获取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对象
干翻Mybatis源码系列之第十篇:Mybatis拦截器基本开发、基本使用和基本细节分析-LMLPHP

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

    @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拦截器基本开发、基本使用和基本细节分析-LMLPHP

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

        String sql = (String) metaObject.set("target.delegate.boundSql.sql","select * from user where id = ?");
06-16 22:47