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 性能监控、自动分页、多租户隔离、公共字段自动填充等功能,提高开发效率和系统可维护性。
在实际应用中,需要根据具体场景选择合适的拦截点,并遵循最佳实践,确保拦截器的正确性和性能。