当前位置: 首页 > news >正文

MyBatis 拦截器的应用场景及实践

MyBatis 作为一款优秀的持久层框架,提供了拦截器(Interceptor)机制,允许开发者在 SQL 执行流程的关键节点插入自定义逻辑。本文将深入探讨拦截器的应用场景,并通过示例代码展示其具体实现。

1.拦截器(Interceptor)概述

在 MyBatis 中,是通过实现 `Interceptor` 接口来拦截特定的方法调用。MyBatis 允许拦截的方法包括:

1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

2. ParameterHandler (getParameterObject, setParameters)

3. ResultSetHandler (handleResultSets, handleOutputParameters)

4. StatementHandler (prepare, parameterize, batch, update, query)

拦截器的执行顺序如下:

Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler

2.应用场景与示例

2.1 SQL 性能监控与日志记录

场景:记录 SQL 执行时间,监控慢查询,辅助性能优化。

示例代码

@Intercepts({@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 PerformanceInterceptor implements Interceptor {private static final Logger logger = LoggerFactory.getLogger(PerformanceInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {long startTime = System.currentTimeMillis();try {// 执行原始方法return invocation.proceed();} finally {long endTime = System.currentTimeMillis();long executeTime = endTime - startTime;// 获取 SQL 信息MappedStatement ms = (MappedStatement) invocation.getArgs()[0];String sqlId = ms.getId();BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);String sql = boundSql.getSql();// 记录 SQL 执行时间
            logger.info("SQL executed in {}ms: {}", executeTime, sql);// 超过 1 秒的 SQL 视为慢查询if (executeTime > 1000) {
                logger.warn("Slow SQL detected ({}ms): {}", executeTime, sqlId);}}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 设置属性}
}

配置方式

<plugins>
    <plugin interceptor="com.example.interceptor.PerformanceInterceptor"/>
</plugins>

2.2 自动分页处理

场景:简化分页查询,自动生成分页 SQL。

示例代码

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = SystemMetaObject.forObject(statementHandler);// 获取原始 SQLBoundSql boundSql = statementHandler.getBoundSql();String originalSql = boundSql.getSql();// 获取参数对象Object parameterObject = boundSql.getParameterObject();// 判断是否为分页查询if (parameterObject instanceof Map) {Map<?, ?> paramMap = (Map<?, ?>) parameterObject;if (paramMap.containsKey("pageNum") && paramMap.containsKey("pageSize")) {int pageNum = (int) paramMap.get("pageNum");int pageSize = (int) paramMap.get("pageSize");// 生成分页 SQL(这里以 MySQL 为例)String paginatedSql = originalSql + " LIMIT " + (pageNum - 1) * pageSize + ", " + pageSize;// 修改 SQL
                metaObject.setValue("boundSql.sql", paginatedSql);}}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 设置属性}
}

使用方式

Map<String, Object> params = new HashMap<>();
params.put("username", "test");
params.put("pageNum", 1);
params.put("pageSize", 10);List<User> users = userMapper.selectByParams(params);

2.3 多租户数据隔离

场景:在多租户系统中,自动为 SQL 添加租户条件,实现数据隔离。

示例代码

@Intercepts({@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 TenantInterceptor implements Interceptor {// 从 ThreadLocal 获取当前租户 IDprivate static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();public static void setTenantId(String tenantId) {
        TENANT_ID.set(tenantId);}public static void clearTenantId() {
        TENANT_ID.remove();}@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];// 获取当前租户 IDString tenantId = TENANT_ID.get();if (tenantId != null) {// 获取 BoundSqlBoundSql boundSql = ms.getBoundSql(parameter);String sql = boundSql.getSql();// 为 SQL 添加租户条件(简化示例,实际应用需更复杂的 SQL 解析)if (!sql.toLowerCase().contains("where")) {
                sql += " WHERE tenant_id = '" + tenantId + "'";} else {
                sql += " AND tenant_id = '" + tenantId + "'";}// 创建新的 BoundSqlBoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, 
                    boundSql.getParameterMappings(), boundSql.getParameterObject());// 处理附加参数for (String key : boundSql.getParameterMappings().keySet()) {if (boundSql.hasAdditionalParameter(key)) {
                    newBoundSql.setAdditionalParameter(key, boundSql.getAdditionalParameter(key));}}// 创建新的 MappedStatementMappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));// 替换原始参数
            invocation.getArgs()[0] = newMs;}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 设置属性}// 辅助方法:复制 MappedStatementprivate MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {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());if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);}
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());return builder.build();}// 辅助类:实现 SqlSource 接口private static class BoundSqlSqlSource implements SqlSource {private BoundSql boundSql;public BoundSqlSqlSource(BoundSql boundSql) {this.boundSql = boundSql;}@Overridepublic BoundSql getBoundSql(Object parameterObject) {return boundSql;}}
}

使用方式

try {// 设置当前租户 IDTenantInterceptor.setTenantId("tenant_001");// 执行数据库操作,自动添加租户条件List<User> users = userService.getUsers();
} finally {// 清除租户 IDTenantInterceptor.clearTenantId();
}

2.4 自动填充公共字段

场景:自动填充创建时间、更新时间、创建人、更新人等公共字段。

示例代码

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];// 只处理插入和更新操作if (ms.getSqlCommandType() == SqlCommandType.INSERT || 
            ms.getSqlCommandType() == SqlCommandType.UPDATE) {// 获取当前用户信息(示例中简化获取方式)String currentUser = getCurrentUser();Date currentTime = new Date();// 使用反射设置公共字段if (parameter != null) {MetaObject metaObject = SystemMetaObject.forObject(parameter);// 插入操作填充创建时间和创建人if (ms.getSqlCommandType() == SqlCommandType.INSERT) {if (metaObject.hasSetter("createTime")) {
                        metaObject.setValue("createTime", currentTime);}if (metaObject.hasSetter("createBy")) {
                        metaObject.setValue("createBy", currentUser);}}// 更新操作填充更新时间和更新人if (metaObject.hasSetter("updateTime")) {
                    metaObject.setValue("updateTime", currentTime);}if (metaObject.hasSetter("updateBy")) {
                    metaObject.setValue("updateBy", currentUser);}}}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 设置属性}// 获取当前用户信息private String getCurrentUser() {// 实际应用中可能从 Session 或 SecurityContext 中获取return "system";}
}

3.过滤器与拦截器的区别

虽然在 MyBatis 中过滤器和拦截器指的是同一概念,但在其他框架(如 Servlet、Spring)中,它们有明显区别:

特性

MyBatis 拦截器

Servlet 过滤器

Spring 拦截器

拦截对象

Executor、StatementHandler 等

HTTP 请求和响应

Spring MVC 处理器

应用场景

SQL 增强、性能监控、参数处理等

请求预处理、编码转换、权限控制等

请求预处理、日志记录、性能监控等

配置方式

实现 Interceptor 接口并注册

实现 Filter 接口并配置

实现 HandlerInterceptor 接口

执行顺序

按注册顺序执行

按配置顺序执行

按注册顺序执行

4.最佳实践

1. 谨慎使用:拦截器会影响所有被拦截的方法,过度使用可能导致代码复杂度增加。

2. 性能考虑:避免在拦截器中执行耗时操作,特别是在高并发场景下。

3. 参数验证:在拦截器中进行参数验证时,确保不影响原有业务逻辑。

4. 异常处理:拦截器中应捕获并处理异常,避免影响主流程。

5. 明确拦截范围:通过 @Signature 精确定义拦截的方法,避免不必要的拦截。

6. 线程安全:确保拦截器是线程安全的,避免共享状态。

5.总结

MyBatis 的过滤器(拦截器)机制为开发者提供了强大的扩展点,可以在不修改原有代码的情况下增强 SQL 执行流程。通过合理使用拦截器,可以实现 SQL 性能监控、自动分页、多租户隔离、公共字段自动填充等功能,提高开发效率和系统可维护性。

在实际应用中,需要根据具体场景选择合适的拦截点,并遵循最佳实践,确保拦截器的正确性和性能。

相关文章:

  • 矩阵链乘法问题
  • Vue:axios(POST请求)
  • 基于线性回归的短期预测
  • 5月26日复盘-自注意力机制
  • 如何提高 Python 代码质量
  • 56页 @《人工智能生命体 新启点》中國龍 原创连载
  • 小巧高效的目录索引生成软件
  • 大模型的检索增强生成综述研究
  • 消费电子卷入“技术军备竞赛”
  • 华为OD机试真题——二叉树的广度优先遍历(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
  • 开卡包的期望
  • CSS闯关指南:从手写地狱到“类”积木之旅|得物技术
  • 尚硅谷redis7 37 redis持久化之AOF简介
  • 2.1 C++之条件语句
  • Nuxt.js vs Next.js:Vue 与 React 阵营的 SSR 双雄对比
  • 传输线上的信号速度与阻抗无关,主要由频率决定
  • Neo4j(三) - 使用Java操作Neo4j详解
  • BaseProviderMultiAdapter多布局总结
  • screen用法
  • AI预测3D新模型百十个定位预测+胆码预测+去和尾2025年5月26日第89弹
  • wordpress图片主题破解版/厦门seo专业培训学校
  • 网站做301对优化有影响/考证培训机构报名网站
  • 欧美化妆品网站模板/专业推广引流团队
  • 合肥网站搜索引擎优化/国内做seo最好的公司
  • 网站开发 动易/获客软件排名前十名
  • 怎样做档口批发网站/武汉网站开发公司seo