MybatisPlus - Interceptor(拦截器)的功能点
01 Interceptor 框架
Interceptor(拦截器)
它的核心任务就是 拦截执行 SQL 语句这一关键动作 ,从而让我们有机会在 SQL 执行前后加入自定义逻辑,实现各种强大的功能拓展。
整个 Interceptor 框架主要由两个关键角色:
• InterceptorChain :扮演着 “拦截器链” 的角色,负责把所有拦截器收入囊中,并以链式调用的方式依次触发它们发挥作用。
• Plugin :像个 “把关人”,判断拦截器是否需要对某个 Handler 的方法实施拦截。
若判定需要拦截,便会量身定制一个代理对象,为后续拦截操作铺好路。
在 Mybatis 配置初始化时,会雷打不动地创建出一个 InterceptorChain 对象。
它肩负着保管所有拦截器的使命,同时提供一个 pulginAll 方法,专门用于驱动拦截器中的 plugin 方法。
每当 Mybatis 去生成 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这些关键角色时,都会挨个询问每个拦截器的 plugin 方法:“需要我为你做点啥吗?” 一旦拦截器有需求,就可以通过代理对象对相应方法调用实现精准拦截。
Mybatis 提供的 Interceptor 接口心怀善意地奉上了一个默认的 plugin 方法,它会踏入 wrap 方法的领地。
在这里,将决定是否要为某个对象打造一个 “代理分身”,以便拦截该对象后续每一个方法的调用。
那么,究竟在啥时候需要这个代理对象呢?
这就得隆重请出与 Mybatis Interceptor 情谊深厚的两个注解了:
• @Intercepts :宛如一张 “拦截器身份卡”,向外界宣告这是一个 Mybatis 拦截器。
• @Signature :负责指明需要代理类的方法,把拦截的目标范围给精准框定出来,可同时配置多个。
使用起来是这样的:通过 @Intercepts 结合 @Signature,把想要拦截的类、方法名、参数类型都设定好,拦截器就能按图索骥去匹配相应对象。
若匹配成功,代理对象即刻登场,把目标对象的方法调用纳入 “监控” 范围。
简而言之,Interceptor 框架在 Mybatis 中就是把握 SQL 执行关键节点的 “幕后推手”,让我们得以灵活地在 SQL 执行流程里插入自定义操作。
02 SQL 执行 “关键卡位”
Mybatis 开放了四个扩展点。
这四个扩展点分别是:
1. ParameterHandler :专门处理查询参数,是把程序中的参数映射到 SQL 语句占位符上的重要关卡。
就好比在 SQL 执行前的 “参数检查站”,确保参数能准确无误地传递给数据库。
2. ResultSetHandler :聚焦于结果集处理,负责把数据库返回的结果集一行行地转换成 Java 对象,是把数据库数据 “翻译” 成程序可用数据格式的关键枢纽。
3. StatementHandler :掌控 SQL 处理及执行,几乎可以说是对整个 SQL 执行过程发号施令的 “指挥官”。
它负责把 SQL 发送给数据库并获取执行结果,是与数据库底层交互的核心桥梁。
4. Executor :着眼于整个 SQL 处理过程,负责管理事务、缓存等关键操作,是站在更高层次协调 SQL 执行的 “大管家”。
这四个扩展点就像串起 SQL 执行全程的珍珠,
让我们有机会在 SQL 从准备到执行、再到结果处理的每个重要环节都留下自己的 “印记”,
轻松实现诸如多租户改造、性能优化、日志记录等丰富功能。
03 多租户开发实战
(一)多租户开发的 “必走之路”
开启多租户开发之旅,得先搞定两件大事:
1.处理多租户标识 :要想区分不同租户,肯定需要一个独特的租户标识,并且这个标识要在程序的线程里有个能随时取用的 “小仓库”,不然压根不知道当前操作是为哪个租户服务的。
2.处理执行 SQL :就算有了租户标识,还得想法子把这个标识作为查询条件塞进 SQL 里,不然从数据库捞出来的数据可不分哪部分属于哪个租户,全混在一块儿了。
(二)多租户标识的 “安家之所”
在咱们这个简洁明了的 DEMO 里,打算把请求头里的 “X - tenant - id” 当成租户的标识。
不过,得戳破个 “窗户纸” —— 在真正的大型框架中,仅靠这么简单的标识可没法应对复杂多变的租户区分需求。
那如何把请求头里的标识弄到手,并存放在能全局取用的地方呢?
这时候,Spring 的 Interceptor 和 ThreadLocal 就闪亮登场了。
Spring 的 Interceptor 和 Mybatis 的 Interceptor 长得挺像,不过人家的作用域可不一样。
Spring 的是针对每一次请求 “出手”,Mybatis 的则是在每一次 SQL 查询时 “发力”。
咱们得打造两个新类:
• TenantSpringInterceptor :拦截每一次请求,把藏在请求头里的租户标识给拽出来。
package com.azir.mybatisinterceptor.interceptor;import com.azir.mybatisinterceptor.TenantContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class TenantSpringInterceptor implements HandlerInterceptor {// 在请求处理前,从请求头获取租户标识并存入线程局部变量public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Stringheader= request.getHeader("X-tenant-id");if (header != null) {TenantContext.setTenantId(Integer.valueOf(header));}return true;}// 请求处理完成后,清理线程局部变量中的租户标识public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {TenantContext.clear();}
}
• TenantContext :利用 ThreadLocal 给每一次请求中获取的租户标识安排一个 “小窝”,让它能被全局访问。
package com.azir.mybatisinterceptor;public class TenantContext {// 使用ThreadLocal保存当前线程的租户IDprivate static final ThreadLocal<Integer> CURRENT_TENANT = newThreadLocal<>();// 设置当前线程的租户IDpublic static void setTenantId(Integer tenantId) {CURRENT_TENANT.set(tenantId);}// 获取当前线程的租户IDpublic static Integer getTenantId() {return CURRENT_TENANT.get();}// 清除当前线程的租户IDpublic static void clear() {CURRENT_TENANT.remove();}
}
注解:
1.TenantSpringInterceptor 实现了 Spring 的 HandlerInterceptor 接口,
在 preHandle 方法中获取请求头信息,并通过 TenantContext 的 setTenantId 方法把租户标识存入 ThreadLocal。
2.TenantContext 利用 ThreadLocal 为每个线程提供独立的租户标识存储空间,
确保在同一线程中能随时随地获取到正确的租户标识,且在请求结束后及时清理,避免线程复用导致的数据错乱。
(三)替换执行 SQL 的 “精准操作”
搞定租户标识的存放后,接下来就得琢磨怎么把 SQL 给改了,让它带上租户条件去数据库里精准查找数据。
在 Mybatis 里,每次执行 SQL 都会催生出一个 StatementHandler,同时还会有个 BoundSql 对象相伴而生。
这个 BoundSql 对象就像个 “小本本”,上面写得清清楚楚即将要执行的 SQL 是啥模样。
所以,咱们只要把 BoundSql 里的 SQL 替换了,就能实现对执行 SQL 的 “乾坤大挪移”。
知道了要修改的对象,那得赶紧瞅瞅 StatementHandler 这个大本营,瞅瞅有啥方法能被 Mybatis 的 Interceptor 给盯上。
StatementHandler 里的 prepare 方法一下子就映入眼帘。
从名字和返回值来看,这妥妥的就是个负责初始化获取数据库 Statement 的方法。
那思路就清晰了,咱们在 prepare 方法执行时,把 BoundSql 里的 SQL 给改了,让它带上租户条件。
package com.azir.mybatisinterceptor.interceptor;import com.azir.mybatisinterceptor.TenantContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.sql.Connection;@Component
@Intercepts(@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class, Integer.class}
))
@Slf4j
public class TenantMybatisInterceptor implements Interceptor {// 获取BoundSql中的sql字段,用于后续修改private static final Field SQL_FIELD;static {try {SQL_FIELD = BoundSql.class.getDeclaredField("sql");SQL_FIELD.setAccessible(true);} catch (NoSuchFieldException e) {log.warn("无法获取BoundSql的sql字段");thrownewRuntimeException(e);}}// 拦截prepare方法,在执行SQL之前修改SQLpublic Object intercept(Invocation invocation)throws Throwable {StatementHandlerstatementHandler= (StatementHandler) invocation.getTarget();BoundSqlboundSql= statementHandler.getBoundSql();Stringsql= boundSql.getSql();log.info("原始SQL:{}", sql);// 添加租户过滤条件到SQL中StringmodifiedSql= addTenantFilter(sql, TenantContext.getTenantId());log.info("修改后的SQL:{}", modifiedSql);// 更新BoundSql中的SQLupdateBoundSql(boundSql, modifiedSql);return invocation.proceed();}// 更新BoundSql中的SQLprivatevoidupdateBoundSql(BoundSql boundSql, String sql) {try {SQL_FIELD.set(boundSql, sql);} catch (Exception e) {thrownewRuntimeException(e);}}// 添加租户过滤条件到SQL中private String addTenantFilter(String sql, Integer tenantId) {if (tenantId == null) {log.warn("租户ID为空,无法添加租户过滤条件");return sql;}// 根据SQL的不同结构,插入租户条件if (sql.contains("where")) {intwhere= sql.lastIndexOf("where");if (sql.contains("and")) {return sql.substring(0, where) + " and tenant_id = '" + tenantId + "' and " + sql.substring(where);}return sql.substring(0, where) + " and tenant_id = '" + tenantId + "'";} elseif (sql.contains("insert")) {inti= sql.indexOf(")");sql = sql.substring(0, i) + ",tenant_id" + sql.substring(i);inti1= sql.lastIndexOf(")");sql = sql.substring(0, i1) + "," + tenantId + sql.substring(i1);return sql;} else {return sql + " where tenant_id = '" + tenantId + "'";}}
}
注解:
1.通过 @Intercepts 注解结合 @Signature,精准定位到 StatementHandler 的 prepare 方法,使其成为拦截目标。
当 Mybatis 执行到 StatementHandler 的 prepare 方法时,拦截器就会被触发。
2.在 intercept 方法中,先获取到 StatementHandler 和 BoundSql 对象,进而拿到原始 SQL。
接着调用 addTenantFilter 方法,把租户条件巧妙地拼接到 SQL 中,再通过 updateBoundSql 方法把修改后的 SQL 塞回 BoundSql 里,从而实现了对执行 SQL 的动态改造。
3.addTenantFilter 方法依据 SQL 的不同结构(带 where 子句、insert 语句、无 where 子句等),运用字符串拼接操作,把租户条件恰到好处地插入到 SQL 中,确保每个 SQL 都能带上租户 “标签” 去数据库里精准查找数据。