Mybatis 自定义拦截器与插件开发

本文详细介绍了MyBatis拦截器的工作原理,包括Interceptor的使用、JDK动态代理在生成代理对象中的作用,以及拦截器在SQL监控、分页、数据权限控制等方面的应用。同时讨论了拦截器的注册顺序和执行逻辑。此外,还提及了设计模式在拦截器和开源框架中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {

return Plugin.wrap(target, this);

}

default void setProperties(Properties properties) {

// NOP

}

}

  • intercept :在拦截目标对象的方法时,实际执行的增强逻辑,我们一般在该方法中实现自定义逻辑

  • plugin :用于返回原生目标对象或它的代理对象,当返回的是代理对象的时候,会调用 intercept 方法

  • setProperties :可以用于读取配置文件中通过 property 标签配置的一些属性,设置一些属性变量

看一下 plugin 方法中的 wrap 方法源码:

public static Object wrap(Object target, Interceptor interceptor) {

Map<Class<?>, Set> signatureMap = getSignatureMap(interceptor);

Class<?> type = target.getClass();

Class<?>[] interfaces = getAllInterfaces(type, signatureMap);

if (interfaces.length > 0) {

return Proxy.newProxyInstance(

type.getClassLoader(),

interfaces,

new Plugin(target, interceptor, signatureMap));

}

return target;

}

可以看到,在 wrap 方法中,通过使用jdk动态代理的方式,生成了目标对象的代理对象,在执行实际方法前,先执行代理对象中的逻辑,来实现的逻辑增强。以拦截 Executor 的 query 方法为例,在实际执行前会执行拦截器中的 intercept 方法:

在mybatis中,不同类型的拦截器按照下面的顺序执行:

Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler

以执行 query 方法为例对流程进行梳理,整体流程如下:

1、 Executor 执行 query() 方法,创建一个 StatementHandler 对象

2、 StatementHandler 调用 ParameterHandler 对象的 setParameters() 方法

3、 StatementHandler 调用 Statement 对象的 execute() 方法

4、 StatementHandler 调用 ResultSetHandler 对象的 handleResultSets() 方法,返回最终结果

拦截器能实现什么

========

在对mybatis拦截器有了初步的认识后,来看一下拦截器被普遍应用在哪些方面:

  • sql 语句执行监控可以拦截执行的sql方法,可以打印执行的sql语句、参数等信息,并且还能够记录执行的总耗时,可供后期的sql分析时使用

  • sql 分页查询mybatis中使用的 RowBounds 使用的内存分页,在分页前会查询所有符合条件的数据,在数据量大的情况下性能较差。通过拦截器,可以做到在查询前修改sql语句,提前加上需要的分页参数

  • 公共字段的赋值在数据库中通常会有 createTime , updateTime 等公共字段,这类字段可以通过拦截统一对参数进行的赋值,从而省去手工通过 set 方法赋值的繁琐过程

  • 数据权限过滤在很多系统中,不同的用户可能拥有不同的数据访问权限,例如在多租户的系统中,要做到租户间的数据隔离,每个租户只能访问到自己的数据,通过拦截器改写sql语句及参数,能够实现对数据的自动过滤

除此之外,拦截器通过对上述的4个阶段的介入,结合我们的实际业务场景,还能够实现很多其他功能。

插件定义与注册

=======

在我们自定义的拦截器类实现了 Interceptor 接口后,还需要在类上添加 @Intercepts 注解,标识该类是一个拦截器类。注解中的内容是一个 @Signature 对象的数组,指明自定义拦截器要拦截哪一个类型的哪一个具体方法。其中 type 指明拦截对象的类型, method 是拦截的方法,args 是 method 执行的参数。通过这里可以了解到 mybatis 拦截器的作用目标是在 方法级别 上进行拦截,例如要拦截 Executor 的 query 方法,就在类上添加:

@Intercepts({

@Signature(type = Executor.class,method = “query”, args = { MappedStatement.class, Object.class,

RowBounds.class, ResultHandler.class })

})

如果要拦截多个方法,可以继续以数组的形式往后追加。这里通过添加参数可以确定唯一的拦截方法,例如在 Executor 中存在两个 query 方法,通过上面的参数可以确定要拦截的是下面的第2个方法:

List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql);

List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler);

当编写完成我们自己的插件后,需要向mybatis中注册插件,有两种方式可以使用,第一种直接在 SqlSessionFactory 中配置:

@Bean

public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

sqlSessionFactoryBean.setDataSource(dataSource);

sqlSessionFactoryBean.setPlugins(new Interceptor[]{new ExecutorPlugin()});

return sqlSessionFactoryBean.getObject();

}

第2种是在 mybatis-config.xml 中对自定义插件进行注册:

在前面我们了解了不同类型拦截器执行的固定顺序,那么对于同样类型的多个自定义拦截器,它们的执行顺序是怎样的呢?分别在 plugin 方法和 intercept 中添加输出语句,运行结果如下:

从结果可以看到,拦截顺序是按照注册顺序执行的,但代理逻辑的执行顺序正好相反,最后注册的会被最先执行。这是因为在mybatis中有一个类 InterceptorChain ,在它的 pluginAll() 方法中,会对原生对象 target 进行代理,如果有多个拦截器的话,会对代理类再次进行代理,最终实现一层层的增强 target 对象,因此靠后被注册的拦截器的增强逻辑会被优先执行。从下面的图中可以直观地看出代理的嵌套关系:

在 xml 中注册完成后,在 application.yml 中启用配置文件,这样插件就可以正常运行了:

mybatis:

config-location: classpath:mybatis-config.xml

在了解了插件的基础概念与运行流程之后,通过代码看一下应用不同的拦截器能够实现什么功能。

拦截器使用示例

=======

Executor

========

通过拦截 Executor 的 query 和 update 方法实现对sql的监控,在拦截方法中,打印sql语句、执行参数、实际执行时间:

@Intercepts({

@Signature(type = Executor.class,method = “update”, args = {MappedStatement.class, Object.class}),

@Signature(type = Executor.class,method = “query”, args = { MappedStatement.class, Object.class,

RowBounds.class, ResultHandler.class })})

public class ExecutorPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

System.out.println(“Executor Plugin 拦截 :”+invocation.getMethod());

Object[] queryArgs = invocation.getArgs();

MappedStatement mappedStatement = (MappedStatement) queryArgs[0];

//获取 ParamMap

MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) queryArgs[1];

// 获取SQL

BoundSql boundSql = mappedStatement.getBoundSql(paramMap);

String sql = boundSql.getSql();

log.info("==> ORIGIN SQL: "+sql);

long startTime = System.currentTimeMillis();

Configuration configuration = mappedStatement.getConfiguration();

String sqlId = mappedStatement.getId();

Object proceed = invocation.proceed();

long endTime=System.currentTimeMillis();

long time = endTime - startTime;

printSqlLog(configuration,boundSql,sqlId,time);

return proceed;

}

public static void printSqlLog(Configuration configuration, BoundSql boundSql, String sqlId, long time){

Object parameterObject = boundSql.getParameterObject();

List parameterMappings = boundSql.getParameterMappings();

String sql= boundSql.getSql().replaceAll(“[\s]+”, " ");

StringBuffer sb=new StringBuffer(“==> PARAM:”);

if (parameterMappings.size()>0 && parameterObject!=null){

TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

sql = sql.replaceFirst(“\?”, parameterObject.toString());

} else {

MetaObject metaObject = configuration.newMetaObject(parameterObject);

for (ParameterMapping parameterMapping : parameterMappings) {

String propertyName = parameterMapping.getProperty();

if (metaObject.hasGetter(propertyName)) {

Object obj = metaObject.getValue(propertyName);

String parameterValue = obj.toString();

sql = sql.replaceFirst(“\?”, parameterValue);

sb.append(parameterValue).append(“(”).append(obj.getClass().getSimpleName()).append(“),”);

} else if (boundSql.hasAdditionalParameter(propertyName)) {

Object obj = boundSql.getAdditionalParameter(propertyName);

String parameterValue = obj.toString();

sql = sql.replaceFirst(“\?”, parameterValue);

sb.append(parameterValue).append(“(”).append(obj.getClass().getSimpleName()).append(“),”);

}

}

}

sb.deleteCharAt(sb.length()-1);

}

log.info(“==> SQL:”+sql);

log.info(sb.toString());

log.info(“==> SQL TIME:”+time+" ms");

}

}

执行代码,日志输出如下:

在上面的代码中,通过 Executor 拦截器获取到了 BoundSql 对象,进一步获取到sql的执行参数,从而实现了对sql执行的监控与统计。

StatementHandler

================

下面的例子中,通过改变 StatementHandler 对象的属性,动态修改sql语句的分页:

@Intercepts({

@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class, Integer.class})})

public class StatementPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

metaObject.setValue(“delegate.rowBounds.offset”, 0);

metaObject.setValue(“delegate.rowBounds.limit”, 2);

return invocation.proceed();

}

}

MetaObject 是mybatis提供的一个用于方便、优雅访问对象属性的对象,通过将实例对象作为参数传递给它,就可以通过属性名称获取对应的属性值。虽然说我们也可以通过反射拿到属性的值,但是反射过程中需要对各种异常做出处理,会使代码中堆满难看的 try/catch ,通过 MetaObject 可以在很大程度上简化我们的代码,并且它支持对 Bean 、 Collection 、 Map 三种类型对象的操作。

对比执行前后:

可以看到这里通过改变了分页对象 RowBounds 的属性,动态地修改了分页参数。

ResultSetHandler

================

ResultSetHandler 会负责映射sql语句查询得到的结果集,如果在生产环境中存在一些保密数据,不想在外部系统中展示,那么可能就需要在查询到结果后做一下数据的脱敏处理,这时候就可以使用 ResultSetHandler 对结果集进行改写。

@Intercepts({

@Signature(type= ResultSetHandler.class,method = “handleResultSets”,args = {Statement.class})})

public class ResultSetPlugin implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

System.out.println(“Result Plugin 拦截 :”+invocation.getMethod());

Object result = invocation.proceed();

if (result instanceof Collection) {

Collection objList= (Collection) result;

List resultList=new ArrayList<>();

for (Object obj : objList) {

resultList.add(desensitize(obj));

}

return resultList;

}else {

return desensitize(result);

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

image

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

image

搜集费时费力,能看到此处的都是真爱!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

[外链图片转存中…(img-PqrG5wbW-1712046106733)]

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

[外链图片转存中…(img-eOmlPgxP-1712046106733)]

搜集费时费力,能看到此处的都是真爱!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值