前言
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 = ?");