Mybatis Common Mapper插件机制:Interceptor开发实战
【免费下载链接】Mapper Mybatis Common Mapper - Easy to use 项目地址: https://gitcode.com/gh_mirrors/ma/Mapper
1. 引言:拦截器(Interceptor)在MyBatis生态中的价值
你是否还在为MyBatis原生开发中重复编写分页逻辑、数据权限过滤而烦恼?是否希望在不侵入业务代码的前提下实现SQL执行监控、性能统计等横切关注点?MyBatis的拦截器(Interceptor)机制正是解决这些问题的关键技术。本文将通过实战案例,系统讲解Mybatis Common Mapper框架下拦截器的开发全流程,帮助开发者掌握这一强大扩展能力。
读完本文你将获得:
- 理解MyBatis拦截器的工作原理与执行流程
- 掌握Mybatis Common Mapper中拦截器的开发规范
- 学会实现3个实用拦截器:SQL日志增强、自动分页、数据权限过滤
- 了解拦截器优先级控制与异常处理最佳实践
- 获取拦截器调试与性能优化的专业技巧
2. MyBatis拦截器核心原理
2.1 拦截器工作模型
MyBatis拦截器基于JDK动态代理和责任链模式实现,允许开发者在SQL执行过程中的特定节点插入自定义逻辑。其核心拦截点包括:
2.2 拦截器执行流程
2.3 Mybatis Common Mapper中的拦截器支持
Mybatis Common Mapper框架通过MapperHelper
类提供了对拦截器的增强支持,主要体现在:
- 维护拦截器注册表,支持多拦截器协同工作
- 提供
SqlHelper
工具类简化SQL解析与重构 - 通过
EntityHelper
实现实体类元数据访问 - 封装
Configuration
对象便于拦截器配置管理
3. 拦截器开发基础
3.1 核心API与注解
MyBatis拦截器开发涉及的关键API如下表所示:
类/接口 | 核心方法 | 作用 |
---|---|---|
Interceptor | intercept(Invocation) | 拦截逻辑实现 |
Interceptor | plugin(Object) | 创建代理对象 |
Interceptor | setProperties(Properties) | 设置拦截器属性 |
Invocation | proceed() | 执行被拦截方法 |
@Intercepts | - | 声明拦截目标方法 |
@Signature | - | 定义拦截签名(类型、方法名、参数) |
3.2 开发步骤
拦截器开发的标准流程包括:
- 创建拦截器类实现
Interceptor
接口 - 使用
@Intercepts
和@Signature
注解定义拦截点 - 在
intercept
方法中实现自定义逻辑 - 配置拦截器并添加到MyBatis环境
3.3 基础拦截器模板
import org.apache.ibatis.plugin.*;
import java.util.Properties;
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {java.sql.Connection.class, Integer.class}
)
})
public class BasicInterceptor implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置处理逻辑
Object result = invocation.proceed(); // 执行原方法
// 后置处理逻辑
return result;
}
@Override
public Object plugin(Object target) {
// 只对StatementHandler类型的目标对象应用拦截器
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 保存拦截器配置属性
this.properties = properties;
}
}
4. 实战案例一:SQL执行日志增强拦截器
4.1 需求分析
实现一个能够记录完整SQL语句(含参数值)、执行耗时和结果行数的日志拦截器,解决原生日志只能输出SQL占位符而无法显示实际参数的问题。
4.2 实现方案
4.3 完整代码实现
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import tk.mybatis.mapper.util.StringUtil;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
})
public class EnhancedSqlLogInterceptor implements Interceptor {
private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
if ("prepare".equals(methodName)) {
// 处理SQL准备阶段,记录开始时间
startTimeThreadLocal.set(System.nanoTime());
return invocation.proceed();
}
// 处理查询或更新操作,计算耗时并输出日志
long startTime = startTimeThreadLocal.get();
if (startTime == 0) {
return invocation.proceed();
}
try {
Object result = invocation.proceed();
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
sql = formatSql(sql);
// 计算执行耗时
long elapsedNanos = System.nanoTime() - startTime;
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
// 输出增强日志
String logMessage = String.format("[SQL Execution] Time: %dms, SQL: %s", elapsedMillis, sql);
System.out.println(logMessage); // 实际项目中建议使用日志框架
return result;
} finally {
startTimeThreadLocal.remove();
}
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 可以通过properties配置日志输出级别等参数
}
private String formatSql(String sql) {
if (StringUtil.isEmpty(sql)) {
return "";
}
// 格式化SQL,去除多余空格和换行
return sql.replaceAll("\\s+", " ").trim();
}
}
4.4 配置与使用
在MyBatis配置文件中注册拦截器:
<plugins>
<plugin interceptor="com.example.EnhancedSqlLogInterceptor">
<!-- 可选配置参数 -->
<!-- <property name="logLevel" value="DEBUG"/> -->
</plugin>
</plugins>
在Spring Boot环境中,可通过Java配置类注册:
@Configuration
public class MyBatisConfig {
@Bean
public EnhancedSqlLogInterceptor enhancedSqlLogInterceptor() {
return new EnhancedSqlLogInterceptor();
}
}
5. 实战案例二:通用分页拦截器
5.1 需求分析
实现一个无需修改Mapper接口即可自动添加分页条件的拦截器,支持主流数据库(MySQL、Oracle、SQL Server)的分页语法。
5.2 实现方案
5.3 核心代码实现
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import tk.mybatis.mapper.entity.Config;
import tk.mybatis.mapper.mapperhelper.MapperHelper;
import tk.mybatis.mapper.util.MsUtil;
import java.util.Properties;
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class UniversalPaginationInterceptor implements Interceptor {
private MapperHelper mapperHelper = new MapperHelper();
private String dialect = "mysql"; // 默认MySQL方言
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取拦截参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
// 无需分页的情况,直接执行原方法
if (rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
}
// 获取SQL语句
BoundSql boundSql = ms.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 根据方言生成分页SQL
String paginationSql = generatePaginationSql(originalSql, rowBounds);
// 创建新的MappedStatement,替换SQL语句
MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(paginationSql, boundSql));
// 修改参数,使用默认RowBounds
args[0] = newMs;
args[2] = RowBounds.DEFAULT;
return invocation.proceed();
}
private String generatePaginationSql(String originalSql, RowBounds rowBounds) {
long offset = rowBounds.getOffset();
long limit = rowBounds.getLimit();
switch (dialect.toLowerCase()) {
case "mysql":
return originalSql + " LIMIT " + offset + ", " + limit;
case "oracle":
return "SELECT * FROM (SELECT t.*, ROWNUM rn FROM (" + originalSql + ") t " +
"WHERE ROWNUM <= " + (offset + limit) + ") WHERE rn > " + offset;
case "sqlserver":
return "SELECT TOP " + limit + " * FROM (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RowNum, * " +
"FROM (" + originalSql + ") AS T) AS Temp WHERE RowNum > " + offset;
default:
throw new UnsupportedOperationException("不支持的数据库方言: " + dialect);
}
}
private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
// 创建新的MappedStatement.Builder对象,复制原有属性并替换SQL源
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
// 复制其他必要属性
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
// ... 省略其他属性复制代码
return builder.build();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 获取配置的数据库方言
if (properties.containsKey("dialect")) {
this.dialect = properties.getProperty("dialect");
}
// 初始化MapperHelper
Config config = new Config();
config.setProperties(properties);
mapperHelper.setConfig(config);
}
// 内部类:包装修改后的SQL
public static class BoundSqlSqlSource implements SqlSource {
private String sql;
private BoundSql boundSql;
public BoundSqlSqlSource(String sql, BoundSql boundSql) {
this.sql = sql;
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
5.4 多数据库支持与配置
配置不同数据库方言:
<plugin interceptor="com.example.UniversalPaginationInterceptor">
<property name="dialect" value="oracle"/> <!-- 可选值:mysql, oracle, sqlserver -->
</plugin>
使用示例:
// Service层代码
public List<User> findUsersByPage(String keyword, int pageNum, int pageSize) {
RowBounds rowBounds = new RowBounds((pageNum - 1) * pageSize, pageSize);
return userMapper.selectByExampleWithRowbounds(example, rowBounds);
}
6. 实战案例三:数据权限拦截器
6.1 业务场景分析
在多租户系统或复杂权限系统中,需要根据当前用户角色自动过滤数据访问权限。例如:
- 管理员可以查看所有部门数据
- 部门经理只能查看本部门数据
- 普通员工只能查看自己的数据
6.2 实现方案设计
6.3 核心代码实现
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import tk.mybatis.mapper.entity.EntityColumn;
import tk.mybatis.mapper.entity.EntityTable;
import tk.mybatis.mapper.mapperhelper.EntityHelper;
import tk.mybatis.mapper.util.MsUtil;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class DataPermissionInterceptor implements Interceptor {
private PermissionService permissionService = new DefaultPermissionService();
private Set<String> includeMappers = new HashSet<>();
private Set<String> excludeMappers = new HashSet<>();
private static final Pattern FROM_PATTERN = Pattern.compile("from\\s+", Pattern.CASE_INSENSITIVE);
private static final Pattern WHERE_PATTERN = Pattern.compile("where\\s+", Pattern.CASE_INSENSITIVE);
private static final Pattern ORDER_BY_PATTERN = Pattern.compile("order\\s+by\\s+", Pattern.CASE_INSENSITIVE);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 判断是否需要拦截
if (!isNeedIntercept(ms)) {
return invocation.proceed();
}
// 获取当前执行的SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 获取当前Mapper接口名称
String mapperId = ms.getId();
String mapperInterface = MsUtil.getMapperInterface(mapperId);
String methodName = MsUtil.getMethodName(mapperId);
// 构建权限过滤SQL
String permissionSql = permissionService.getPermissionSql(mapperInterface, methodName);
if (StringUtil.isNotEmpty(permissionSql)) {
originalSql = buildPermissionSql(originalSql, permissionSql);
}
// 修改SQL并执行
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", originalSql);
return invocation.proceed();
}
private String buildPermissionSql(String originalSql, String permissionSql) {
originalSql = originalSql.replaceAll("\\s+", " ");
// 查找FROM子句位置
Matcher fromMatcher = FROM_PATTERN.matcher(originalSql);
if (!fromMatcher.find()) {
return originalSql;
}
// 查找WHERE子句位置
int whereIndex = -1;
Matcher whereMatcher = WHERE_PATTERN.matcher(originalSql);
if (whereMatcher.find(fromMatcher.end())) {
whereIndex = whereMatcher.start();
}
// 查找ORDER BY子句位置
int orderByIndex = -1;
Matcher orderByMatcher = ORDER_BY_PATTERN.matcher(originalSql);
if (orderByMatcher.find(fromMatcher.end())) {
orderByIndex = orderByMatcher.start();
}
// 确定权限条件插入位置
int insertPosition;
if (whereIndex != -1 && (orderByIndex == -1 || whereIndex < orderByIndex)) {
// 在WHERE子句后插入
insertPosition = whereIndex + 6; // "WHERE ".length()
return originalSql.substring(0, insertPosition) +
"(" + permissionSql + ") AND " +
originalSql.substring(insertPosition);
} else if (orderByIndex != -1) {
// 在ORDER BY子句前插入
return originalSql.substring(0, orderByIndex) +
" WHERE " + permissionSql + " " +
originalSql.substring(orderByIndex);
} else {
// 在SQL末尾插入
return originalSql + " WHERE " + permissionSql;
}
}
private boolean isNeedIntercept(MappedStatement ms) {
// 只拦截查询操作
if (ms.getSqlCommandType() != SqlCommandType.SELECT) {
return false;
}
String mapperId = ms.getId();
String mapperInterface = MsUtil.getMapperInterface(mapperId);
// 检查是否在排除列表中
if (excludeMappers != null && excludeMappers.length > 0) {
for (String exclude : excludeMappers) {
if (mapperInterface.startsWith(exclude)) {
return false;
}
}
}
// 检查是否在包含列表中
if (includeMappers == null || includeMappers.length == 0) {
return true; // 默认拦截所有查询
}
for (String include : includeMappers) {
if (mapperInterface.startsWith(include)) {
return true;
}
}
return false;
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 配置包含的Mapper
String includeMappersProp = properties.getProperty("includeMappers");
if (StringUtil.isNotEmpty(includeMappersProp)) {
this.includeMappers = includeMappersProp.split(",");
}
// 配置排除的Mapper
String excludeMappersProp = properties.getProperty("excludeMappers");
if (StringUtil.isNotEmpty(excludeMappersProp)) {
this.excludeMappers = excludeMappersProp.split(",");
}
}
// 设置自定义权限服务
public void setPermissionService(PermissionService permissionService) {
this.permissionService = permissionService;
}
}
// 权限服务接口
interface PermissionService {
String getPermissionSql(String mapperInterface, String methodName);
}
// 默认权限服务实现
class DefaultPermissionService implements PermissionService {
@Override
public String getPermissionSql(String mapperInterface, String methodName) {
// 实际项目中应从当前用户上下文获取角色信息
// 这里简化处理,仅作示例
String currentUserRole = SecurityContext.getCurrentUserRole();
String currentDeptId = SecurityContext.getCurrentUserDeptId();
String currentUserId = SecurityContext.getCurrentUserId();
// 根据不同Mapper和角色返回不同权限条件
if ("com.example.mapper.UserMapper".equals(mapperInterface)) {
if ("ADMIN".equals(currentUserRole)) {
return null; // 管理员无限制
} else if ("DEPT_MANAGER".equals(currentUserRole)) {
return "dept_id = " + currentDeptId; // 部门经理只能查看本部门数据
} else {
return "create_user_id = " + currentUserId; // 普通用户只能查看自己创建的数据
}
}
return null;
}
}
6.4 权限规则配置与使用
配置拦截器:
<plugin interceptor="com.example.DataPermissionInterceptor">
<!-- 只拦截指定的Mapper -->
<property name="includeMappers" value="com.example.mapper.UserMapper,com.example.mapper.OrderMapper"/>
<!-- 排除某些Mapper -->
<property name="excludeMappers" value="com.example.mapper.DictMapper"/>
</plugin>
7. 拦截器高级特性
7.1 拦截器优先级控制
MyBatis通过拦截器配置顺序控制执行顺序,配置在前的拦截器会先执行。对于复杂场景,可通过@Order
注解或实现Ordered
接口精确控制:
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class FirstInterceptor implements Interceptor {
// ...实现代码
}
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
public class SecondInterceptor implements Interceptor {
// ...实现代码
}
7.2 异常处理策略
拦截器中的异常处理应遵循以下原则:
- 业务异常:转换为统一异常格式后抛出
- 系统异常:记录详细日志,包装为运行时异常抛出
- 可恢复异常:尝试重试或降级处理
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 拦截器逻辑
return invocation.proceed();
} catch (BusinessException e) {
// 业务异常直接抛出
throw new ServiceException("业务处理失败: " + e.getMessage(), e);
} catch (Exception e) {
// 系统异常记录日志后抛出
logger.error("Interceptor error: " + e.getMessage(), e);
throw new SystemException("系统处理异常", e);
}
}
7.3 动态开关控制
实现可动态开启/关闭的拦截器:
public class ToggleableInterceptor implements Interceptor {
private volatile boolean enabled = true;
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!enabled) {
return invocation.proceed();
}
// 拦截器逻辑
return invocation.proceed();
}
// 提供开关控制方法
public void enable() {
this.enabled = true;
}
public void disable() {
this.enabled = false;
}
// 其他方法实现...
}
通过JMX或配置中心动态控制:
@ManagedResource(objectName = "MyBatis:name=ToggleableInterceptor")
public class InterceptorManager {
@Autowired
private ToggleableInterceptor toggleableInterceptor;
@ManagedOperation
public void enableInterceptor() {
toggleableInterceptor.enable();
}
@ManagedOperation
public void disableInterceptor() {
toggleableInterceptor.disable();
}
@ManagedAttribute
public boolean isInterceptorEnabled() {
return toggleableInterceptor.isEnabled();
}
}
8. 调试与性能优化
8.1 拦截器调试技巧
- 使用MyBatis的
LoggingInterceptor
查看完整执行流程 - 通过
MetaObject
打印目标对象的所有属性:
private void printMetaObject(Object target) {
MetaObject metaObject = SystemMetaObject.forObject(target);
for (String name : metaObject.getGetterNames()) {
try {
Object value = metaObject.getValue(name);
System.out.println(name + " = " + value);
} catch (Exception e) {
// 忽略获取失败的属性
}
}
}
- 使用断点调试时,注意代理对象的层级关系
8.2 性能优化建议
-
减少反射操作:缓存反射结果,避免频繁反射
private static final Map<Class<?>, Field> fieldCache = new ConcurrentHashMap<>(); private Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException { String key = clazz.getName() + "." + fieldName; if (!fieldCache.containsKey(key)) { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); fieldCache.put(key, field); } return fieldCache.get(key); }
-
避免重复处理:使用ThreadLocal缓存中间结果
-
控制拦截范围:通过精确的签名配置减少拦截方法数量
-
异步处理:非关键路径逻辑使用异步处理
private ExecutorService executor = Executors.newCachedThreadPool(); @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); // 异步记录SQL执行 metrics executor.submit(() -> recordMetrics(result)); return result; }
8.3 常见问题与解决方案
问题 | 解决方案 |
---|---|
SQL解析错误 | 使用专门的SQL解析库如JSqlParser,避免正则表达式解析 |
拦截器链执行顺序混乱 | 明确指定拦截器顺序,避免循环依赖 |
性能开销大 | 减少反射操作,缓存元数据,控制拦截范围 |
与其他框架冲突 | 使用@Intercepts 注解精确指定拦截点,避免过度拦截 |
9. 总结与展望
MyBatis拦截器机制为开发者提供了强大的扩展能力,通过本文介绍的三个实战案例,我们展示了如何利用这一机制解决SQL日志增强、自动分页、数据权限过滤等常见问题。拦截器的核心价值在于:
- 无侵入式扩展:不修改原有业务代码即可添加横切功能
- 代码复用:将通用逻辑抽象为拦截器,实现一次开发多处复用
- 灵活配置:通过配置控制拦截器行为,适应不同环境需求
未来发展趋势:
- 基于注解的拦截器配置方式
- AI辅助的SQL优化拦截器
- 更细粒度的拦截点支持
- 与微服务可观测性体系的深度整合
10. 扩展学习资源
-
官方文档:
- MyBatis官方文档:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
- Mybatis Common Mapper文档:https://github.com/abel533/Mapper/wiki
-
推荐工具:
- JSqlParser:SQL解析工具
- MyBatis-Plus:提供更多拦截器扩展
- PageHelper:分页插件参考实现
-
实战项目:
- 分布式追踪:通过拦截器实现SQL执行链路追踪
- 读写分离:基于拦截器实现SQL路由
- 数据脱敏:查询结果敏感信息自动脱敏
通过掌握拦截器开发技巧,开发者可以极大提升MyBatis应用的灵活性和可维护性,应对复杂多变的业务需求。希望本文能为你打开MyBatis高级应用开发的大门。
收藏与分享:如果本文对你有帮助,请收藏并分享给更多开发者!关注作者获取更多MyBatis进阶教程。
下期预告:《Mybatis Common Mapper代码生成器深度定制》—— 教你如何生成符合企业规范的高质量代码。
【免费下载链接】Mapper Mybatis Common Mapper - Easy to use 项目地址: https://gitcode.com/gh_mirrors/ma/Mapper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考