在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 { 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
| 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);
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
|
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
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( new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml")); 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> <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
Be the first person to leave a comment!