【DB系列】SpringBoot系列Mybatis之插件机制Interceptor

文章目录
  1. I. 环境准备
    1. 1. 数据库准备
    2. 2. 项目环境
  2. II. 实例演示
    1. 1. 自定义interceptor
      1. 1.1 sql参数解析说明
      2. 1.2 Intercepts注解
      3. 1.3 切点说明
    2. 2. 插件注册
      1. 2.1 Spring Bean
      2. 2.2 SqlSessionFactory
      3. 2.3 xml配置
    3. 3. 小结
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 一灰灰Blog

在Mybatis中,插件机制提供了非常强大的扩展能力,在sql最终执行之前,提供了四个拦截点,支持不同场景的功能扩展

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

本文将主要介绍一下自定义Interceptor的使用姿势,并给出一个通过自定义插件来输出执行sql,与耗时的case

I. 环境准备

1. 数据库准备

使用mysql作为本文的实例数据库,新增一张表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境

本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

pom依赖如下

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

db配置信息 application.yml

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

II. 实例演示

关于myabtis的配套Entity/Mapper相关内容,推荐查看之前的系列博文,这里就不贴出来了,将主要集中在Interceptor的实现上

1. 自定义interceptor

实现一个自定义的插件还是比较简单的,试下org.apache.ibatis.plugin.Interceptor接口即可

比如定义一个拦截器,实现sql输出,执行耗时输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Component
@Intercepts(value = {@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 ExecuteStatInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// MetaObject 是 Mybatis 提供的一个用于访问对象属性的对象
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
BoundSql sql = statement.getBoundSql(invocation.getArgs()[1]);

long start = System.currentTimeMillis();
List<ParameterMapping> list = sql.getParameterMappings();
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}
try {
return invocation.proceed();
} finally {
System.out.println("------------> sql: " + sql.getSql() + "\n------------> args: " + params + "------------> cost: " + (System.currentTimeMillis() - start));
}
}

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

@Override
public void setProperties(Properties properties) {

}
}

注意上面的实现,核心逻辑在intercept方法,内部实现sql获取,参数解析,耗时统计

1.1 sql参数解析说明

上面case中,对于参数解析,mybatis是借助Ognl来实现参数替换的,因此上面直接使用ognl表达式来获取sql参数,当然这种实现方式比较粗暴

1
2
3
4
5
6
// 下面这一段逻辑,主要是OGNL的使用姿势
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}

除了上面这种姿势之外,我们知道最终mybatis也是会实现sql参数解析的,如果有分析过源码的小伙伴,对下面这种姿势应该比较熟悉了

源码参考自: org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

1
2
3
4
5
6
7
8
9
10
11
12
BoundSql sql = statementHandler.getBoundSql();
DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler();
Field field = handler.getClass().getDeclaredField("configuration");
field.setAccessible(true);
Configuration configuration = (Configuration) ReflectionUtils.getField(field, handler);
// 这种姿势,与mybatis源码中参数解析姿势一直
//
MetaObject mo = configuration.newMetaObject(sql.getParameterObject());
List<Object> args = new ArrayList<>();
for (ParameterMapping key : sql.getParameterMappings()) {
args.add(mo.getValue(key.getProperty()));
}

但是使用上面这种姿势,需要注意并不是所有的切点都可以生效;这个涉及到mybatis提供的四个切点的特性,这里也就不详细进行展开,在后面的源码篇,这些都是绕不过去的点

1.2 Intercepts注解

接下来重点关注一下类上的@Intercepts注解,它表明这个类是一个mybatis的插件类,通过@Signature来指定切点

其中的type, method, args用来精确命中切点的具体方法

如根据上面的实例case进行说明

1
2
3
@Intercepts(value = {@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, update,参数类型也一并定义了,通过这些信息,可以精确匹配Executor接口上定义的类,如下

1
2
3
4
5
6
7
// org.apache.ibatis.executor.Executor

// 对应第一个@Signature
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

// 对应第二个@Signature
int update(MappedStatement var1, Object var2) throws SQLException;

1.3 切点说明

mybatis提供了四个切点,那么他们之间有什么区别,什么样的场景选择什么样的切点呢?

一般来讲,拦截ParameterHandler是最常见的,虽然上面的实例是拦截Executor,切点的选择,主要与它的功能强相关,想要更好的理解它,需要从mybatis的工作原理出发,这里将只做最基本的介绍,待后续源码进行详细分析

  • Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
  • StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  • ParameterHandler:是用来处理SQL参数的。
  • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。

借用网上的一张mybatis执行过程来辅助说明

原文 https://blog.csdn.net/weixin_39494923/article/details/91534658

2. 插件注册

上面只是自定义插件,接下来就是需要让这个插件生效,也有下面几种不同的姿势

2.1 Spring Bean

将插件定义为一个普通的Spring Bean对象,则可以生效

2.2 SqlSessionFactory

直接通过SqlSessionFactory来注册插件也是一个非常通用的做法,正如之前注册TypeHandler一样,如下

1
2
3
4
5
6
7
8
9
10
11
12
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(
// 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
// 注册typehandler,供全局使用
bean.setTypeHandlers(new Timestamp2LongHandler());
bean.setPlugins(new SqlStatInterceptor());
return bean.getObject();
}

2.3 xml配置

习惯用mybatis的xml配置的小伙伴,可能更喜欢使用下面这种方式,在mybatis-config.xml全局xml配置文件中进行定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 驼峰下划线格式支持 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.git.hui.boot.mybatis.entity"/>
</typeAliases>

<!-- type handler 定义 -->
<typeHandlers>
<typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
</typeHandlers>

<!-- 插件定义 -->
<plugins>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.SqlStatInterceptor"/>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.ExecuteStatInterceptor"/>
</plugins>
</configuration>

3. 小结

本文主要介绍mybatis的插件使用姿势,一个简单的实例演示了如果通过插件,来输出执行sql,以及耗时

自定义插件实现,重点两步

  • 实现接口org.apache.ibatis.plugin.Interceptor
  • @Intercepts 注解修饰插件类,@Signature定义切点

插件注册三种姿势:

  • 注册为Spring Bean
  • SqlSessionFactory设置插件
  • myabtis.xml文件配置

III. 不能错过的源码和相关知识点

0. 项目

mybatis系列博文

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到