MyBatis 拦截器完整使用指南

MyBatis 拦截器完整使用指南

引言

MyBatis 拦截器(Interceptor)是 MyBatis 强大的扩展机制之一,它允许我们在 SQL 语句执行的不同阶段插入自定义逻辑。通过拦截器,我们可以实现分页插件、权限校验、SQL 日志、自动填充等功能。本文将深入讲解 MyBatis 拦截器的原理、使用场景、编写方法以及实际应用。

一、拦截器的工作原理

1.1 拦截器机制概述

MyBatis 的拦截器基于责任链设计模式实现,属于高级别的扩展机制。它可以在 SQL 执行的四个关键节点进行拦截和处理:

  • 参数预处理(ParameterHandler):在执行 SQL 之前,对参数进行预处理
  • SQL 改写(StatementHandler):拦截 JDBC Statement 对象,用于修改 SQL 语句
  • 语句执行(PreparedStatement):在执行 Statement 之前进行拦截
  • 结果集转换(ResultSetHandler):处理查询结果,可用于分页、结果加密等操作

1.2 核心组件关系

Executor → StatementHandler → PreparedStatement → Statement
     ↓              ↓                      ↓           ↓
  Interceptor  Interceptor            Interceptor   Interceptor

当 MyBatis 执行数据库操作时,会按照上述顺序依次经过各个拦截点。每个拦截器都实现了 `Interceptor` 接口,并可以在 `@Intercepts` 注解中声明要拦截的签名。

二、拦截器的使用场景

2.1 分页插件

最经典的应用场景就是分页功能。PageHelper 等分页插件就是通过拦截器拦截 SQL 语句,动态添加 `LIMIT` 子句来实现的。

2.2 数据权限控制

拦截器可以读取当前用户信息,自动在查询条件中添加权限过滤,如数据行级权限、部门隔离等。

2.3 自动填充

实现创建时间、更新时间、创建人、更新人等字段的自动填充功能。

2.4 SQL 日志记录

拦截 SQL 语句和执行参数,记录完整的 SQL 日志,便于调试和性能分析。

2.5 性能优化

通过拦截器实现 SQL 缓存、结果集缓存等功能。

三、编写自定义拦截器

3.1 实现基本结构

创建拦截器需要实现 MyBatis 的 `Interceptor` 接口,该接口包含三个核心方法:

“`java
@Intercepts({
@Signature(
type = Executor.class,
method = “query”,
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class MyInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 拦截处理逻辑
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 属性配置
}
}


3.2 方法详解

intercept 方法:这是拦截器的核心方法,所有拦截逻辑都在这里实现。`invocation` 对象包含了目标对象、方法和参数,调用 `invocation.proceed()` 可以继续执行原始方法。 plugin 方法:MyBatis 使用 JDK 动态代理来实现拦截。此方法返回目标对象的代理对象,只有被代理的对象上的方法才会被拦截。 setProperties 方法:当拦截器被配置在 `mybatis-config.xml` 中时,可以传入自定义属性,在此方法中接收和解析这些属性。

3.3 完整的拦截器示例

以下是一个实现 SQL 日志和性能分析的拦截器示例:

java
@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);

@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();

String sql = getSql(invocation);
logger.info(“执行 SQL: {}”, sql);

Object result = invocation.proceed();

long duration = System.currentTimeMillis() – startTime;
logger.info(“SQL 执行耗时:{} ms”, duration);

if (duration > 1000) {
logger.warn(“SQL 执行耗时超过 1 秒:{} ms”, duration);
}

return result;
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 处理配置属性
}

private String getSql(Invocation invocation) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String sql = ms.getBoundSql(invocation.getArgs()[1]).getSql();
return sql.replace(“\n”, “”).replace(“\r”, “”).replaceAll(“\\s+”, ” “);
}
}


四、实际应用场景

4.1 分页拦截器实现

java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = “prepare”,
args = {Connection.class, Integer.class}
)
})
public class SimplePaginationInterceptor implements Interceptor {

private PageContext pageContext = new PageContext();

@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();

Object paramObj = boundSql.getParameters();
Map paramMap = new HashMap<>();
if (paramObj instanceof Map) {
paramMap = (Map) paramObj;
} else if (paramObj instanceof PageContext) {
pageContext = (PageContext) paramObj;
paramMap = ((PageContext) paramObj).getParams();
}

if (pageContext != null && pageContext.isPage()) {
String sql = boundSql.getSql();
String countSql = countSql(sql);
// 添加分页 SQL
String pageSql = addPagination(sql, pageContext);
boundSql.setSql(pageSql);
}

return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

private String countSql(String sql) {
// 将 SELECT 语句转换为 COUNT 查询
return sql.replaceAll(“SELECT.* FROM”, “SELECT COUNT(*) FROM”, true);
}

private String addPagination(String sql, PageContext pageContext) {
return sql + ” LIMIT ” + pageContext.getOffset() + “,” + pageContext.getLimit();
}
}


4.2 数据权限拦截器

java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = “prepare”,
args = {Connection.class, Integer.class}
)
})
public class DataPermissionInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();

String sql = boundSql.getSql();
Object parameter = boundSql.getParameters();

// 获取当前用户信息
UserInfo currentUser = SecurityContext.getCurrentUser();

// 根据用户权限添加数据过滤条件
String whereClause = generateWhereClause(currentUser);
if (whereClause != null && !whereClause.isEmpty()) {
sql = addWhereCondition(sql, whereClause);
}

boundSql.setSql(sql);

return invocation.proceed();
}

private String generateWhereClause(UserInfo user) {
if (user.getDepartmentId() != null) {
return String.format(” AND department_id = %d”, user.getDepartmentId());
}
return “”;
}

private String addWhereCondition(String sql, String condition) {
int index = sql.toUpperCase().indexOf(” WHERE “);
if (index == -1) {
return sql + ” WHERE 1=1 ” + condition;
}
// 复杂场景需要更精细的 SQL 解析
return sql + ” AND 1=1″ + condition;
}
}


4.3 自动填充拦截器

java
@Intercepts({
@Signature(
type = ParameterHandler.class,
method = “setParameters”,
args = {PreparedStatement.class}
)
})
public class AutoFillInterceptor implements Interceptor {

private static final String UPDATE_TIME = “updateTime”;
private static final String CREATE_TIME = “createTime”;
private static final String UPDATE_USER = “updateUser”;
private static final String CREATE_USER = “createUser”;

@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler handler = (ParameterHandler) invocation.getTarget();
PreparedStatement statement = (PreparedStatement) invocation.getArgs()[0];

BoundSql boundSql = handler.getBoundSql();
Object parameter = boundSql.getParameters();

if (shouldAutoFill(parameter)) {
fillValue(parameter);
}

return invocation.proceed();
}

private boolean shouldAutoFill(Object parameter) {
if (parameter instanceof Map) {
Map paramMap = (Map) parameter;
return paramMap.containsKey(“updateTime”) || paramMap.containsKey(“createTime”);
}
return false;
}

private void fillValue(Object parameter) {
Map paramMap = (Map) parameter;

// 设置时间
paramMap.put(UPDATE_TIME, new Date());
if (!paramMap.containsKey(CREATE_TIME)) {
paramMap.put(CREATE_TIME, new Date());
}

// 设置用户
String currentUser = SecurityContext.getCurrentUser().getUsername();
paramMap.put(UPDATE_USER, currentUser);
if (!paramMap.containsKey(CREATE_USER)) {
paramMap.put(CREATE_USER, currentUser);
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 配置属性
}
}


五、配置与注册

5.1 MyBatis 配置文件

在 `mybatis-config.xml` 中注册拦截器:

xml


5.2 Spring Boot 配置

如果使用 Spring Boot,可以通过配置类注册拦截器:

java
@Configuration
public class MyBatisConfig {

@Bean
public Interceptor pageInterceptor() {
return new PageInterceptor();
}

@Bean
public Interceptor performanceInterceptor() {
return new PerformanceInterceptor();
}

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(pageInterceptor());
interceptor.addInnerInterceptor(performanceInterceptor());
return interceptor;
}
}
“`

六、注意事项与最佳实践

6.1 性能考虑

  • 拦截器会在每次 SQL 执行时调用,避免在 `intercept` 方法中执行耗时操作
  • 使用异步日志或批量记录来减少性能影响
  • 对于高并发场景,考虑使用缓存减少重复计算

6.2 错误处理

  • 在拦截器中妥善处理异常,避免影响正常的业务逻辑
  • 记录详细的错误日志,便于问题定位
  • 避免在拦截器中抛出未处理的异常

6.3 配置管理

  • 将拦截器的配置参数外部化,便于根据不同环境调整
  • 使用配置文件管理拦截器开关,方便启用或禁用特定功能
  • 为每个拦截器添加独立的配置命名空间

6.4 版本兼容性

  • 注意 MyBatis 版本之间的 API 变化
  • 使用稳定的 API 签名,避免内部实现的变更
  • 定期测试拦截器在升级后的兼容性

七、总结

MyBatis 拦截器是强大的扩展机制,通过理解其工作原理并合理应用,可以解决许多常见的问题。无论是分页、权限控制、性能监控还是自动填充,拦截器都能提供优雅的解决方案。

记住以下要点:

  1. 选择合适的拦截点:根据需求选择 `Executor`、`StatementHandler` 等拦截点
  2. 保持拦截器轻量:避免在拦截器中执行耗时操作
  3. 良好的错误处理:确保拦截器异常不影响业务逻辑
  4. 合理的配置管理:使用配置文件管理拦截器参数
  5. 充分测试:在不同场景下测试拦截器的功能和性能
  6. 掌握 MyBatis 拦截器,你将能够更灵活地扩展和定制 MyBatis 的行为,满足各种复杂的业务需求。

    参考资料

    • [MyBatis 官方文档](https://mybatis.org/mybatis-3/zh/configuration.html)
    • [PageHelper 分页插件](https://pagehelper.gitee.io/pagehelper/)
    • [MyBatis-Plus 文档](https://baomidou.com/pages/221785/#interceptor)

标签

发表评论