赞
踩
【SpringBoot + Mybatis 系列】Mybatis 之插件机制 Interceptor
在 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
使用 mysql 作为本文的实例数据库,新增一张表
- 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;
本文借助 SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
pom 依赖如下
- <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
- spring:
- datasource:
- url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
- username: root
- password:
关于 myabtis 的配套 Entity/Mapper 相关内容,推荐查看之前的系列博文,这里就不贴出来了,将主要集中在 Interceptor 的实现上
实现一个自定义的插件还是比较简单的,试下org.apache.ibatis.plugin.Interceptor
接口即可
比如定义一个拦截器,实现 sql 输出,执行耗时输出
- @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 获取,参数解析,耗时统计
上面 case 中,对于参数解析,mybatis 是借助 Ognl 来实现参数替换的,因此上面直接使用 ognl 表达式来获取 sql 参数,当然这种实现方式比较粗暴
- // 下面这一段逻辑,主要是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
- 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 提供的四个切点的特性,这里也就不详细进行展开,在后面的源码篇,这些都是绕不过去的点
接下来重点关注一下类上的@Intercepts
注解,它表明这个类是一个 mybatis 的插件类,通过@Signature
来指定切点
其中的 type, method, args 用来精确命中切点的具体方法
如根据上面的实例 case 进行说明
- @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
接口上定义的类,如下
- // 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;
mybatis 提供了四个切点,那么他们之间有什么区别,什么样的场景选择什么样的切点呢?
一般来讲,拦截ParameterHandler
是最常见的,虽然上面的实例是拦截Executor
,切点的选择,主要与它的功能强相关,想要更好的理解它,需要从 mybatis 的工作原理出发,这里将只做最基本的介绍,待后续源码进行详细分析
Executor:代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL,其中 StatementHandler 是最重要的。
StatementHandler:作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
ParameterHandler:是用来处理 SQL 参数的。
ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。
借用网上的一张 mybatis 执行过程来辅助说明
上面只是自定义插件,接下来就是需要让这个插件生效,也有下面几种不同的姿势
将插件定义为一个普通的 Spring Bean 对象,则可以生效
直接通过SqlSessionFactory
来注册插件也是一个非常通用的做法,正如之前注册 TypeHandler 一样,如下
- @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();
- }
习惯用 mybatis 的 xml 配置的小伙伴,可能更喜欢使用下面这种方式,在mybatis-config.xml
全局 xml 配置文件中进行定义
- <?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>

本文主要介绍 mybatis 的插件使用姿势,一个简单的实例演示了如果通过插件,来输出执行 sql,以及耗时
自定义插件实现,重点两步
实现接口org.apache.ibatis.plugin.Interceptor
@Intercepts
注解修饰插件类,@Signature
定义切点
插件注册三种姿势:
注册为 Spring Bean
SqlSessionFactory 设置插件
myabtis.xml 文件配置
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。