【mybatis拦截器】深入剖析:是什么、为什么、哪里、多少、如何、怎么
MyBatis作为一个优秀的持久层框架,提供了强大的扩展机制,其中拦截器(Interceptor)便是其核心之一。它允许我们在不修改MyBatis核心源码的前提下,对MyBatis的内部行为进行干预和增强。本文将围绕MyBatis拦截器的方方面面展开详细探讨,帮助读者全面理解并熟练运用这一强大功能。
是什么:MyBatis拦截器的工作原理与核心组件
MyBatis拦截器是MyBatis提供的一种插件机制,它基于Java的动态代理技术,允许开发者在MyBatis执行SQL的特定生命周期点插入自己的逻辑。简单来说,它充当了MyBatis核心组件与外部逻辑之间的“中间人”,在方法调用前后进行额外的处理。
核心接口与注解
org.apache.ibatis.plugin.Interceptor接口: 这是所有MyBatis拦截器必须实现的接口。它包含三个核心方法:Object intercept(Invocation invocation) throws Throwable;:这是拦截器的核心逻辑所在。它接收一个Invocation对象,通过调用invocation.proceed()来执行被拦截的原始方法,或者在执行前后添加自定义逻辑,甚至完全替换原始方法的执行。Object plugin(Object target);:这个方法是用于包装目标对象的。MyBatis会判断当前拦截器是否应该拦截这个target对象。如果需要拦截,则返回一个代理对象(通常由Plugin.wrap(target, this)生成);否则,返回target本身。void setProperties(Properties properties);:此方法在拦截器初始化时被调用,允许我们在配置文件中为拦截器传递属性。
org.apache.ibatis.plugin.Signature注解: 这个注解用于声明当前拦截器要拦截的具体方法。一个拦截器可以声明多个@Signature,每个@Signature定义了一个拦截点。
@Signature有三个属性:type():指定要拦截的MyBatis核心组件类型,必须是Executor、StatementHandler、ParameterHandler或ResultSetHandler之一。method():指定要拦截的组件中的方法名。args():指定要拦截的方法的参数类型列表,用于精确匹配方法签名。
为什么:MyBatis拦截器的应用场景与优势
为什么我们需要MyBatis拦截器?答案在于它提供了一种非侵入式的、高度灵活的扩展方式,特别适用于那些需要对SQL执行过程进行统一处理的场景。
相较于其他扩展方式的优势
- 非侵入性: 无需修改MyBatis的源码或Mapper接口代码,通过配置即可启用或禁用拦截器。
- 集中式管理: 将跨越多个Mapper的通用逻辑集中在一个地方处理,避免代码冗余。
- MyBatis生命周期感知: 能够精确地在MyBatis SQL执行过程中的特定阶段插入逻辑,这是其他AOP框架难以直接做到的。
- 业务解耦: 将技术层面的横切关注点(如数据权限、多租户、字段加解密、审计日志)与业务逻辑分离。
典型的应用场景
- 数据权限控制: 在SQL查询执行前,根据用户权限动态修改SQL,添加WHERE条件。
- 多租户支持: 根据当前租户ID,自动为所有查询和插入操作添加或过滤租户ID字段。
- 字段加解密: 在参数传入数据库前加密,从数据库取出后解密敏感字段。
- 性能监控与日志记录: 记录SQL执行时间、SQL语句、参数等信息,用于性能分析或审计。
- 自动填充字段: 自动设置创建时间、更新时间、创建人、更新人等公共字段。
- 逻辑删除: 将DELETE操作转换为UPDATE操作,更新记录的
is_deleted标志。 - 结果集处理: 对从数据库取出的结果集进行二次处理,如数据脱敏、枚举转换。
哪里:MyBatis拦截器能拦截的四大核心组件
MyBatis的拦截器机制允许我们对SQL执行过程中的四个核心组件进行拦截。理解这些组件在MyBatis生命周期中的作用,是正确选择拦截点的关键。
1. Executor (执行器)
- 作用: 负责MyBatis一级缓存和二级缓存的维护、事务管理以及SQL的最终执行。它是MyBatis执行流程中最高层的抽象。
- 可拦截方法:
update(),query(),commit(),rollback(),close()等。 - 典型应用:
- 全局性能统计: 拦截
query和update方法,统计所有SQL的执行耗时。 - 全局日志: 记录每一次数据库操作的详细信息。
- 二级缓存扩展: 对MyBatis的二级缓存行为进行自定义干预。
- 全局性能统计: 拦截
2. StatementHandler (语句处理器)
- 作用: 负责构建SQL语句、设置预编译参数以及执行SQL语句。它是将SQL语句发送到数据库前的关键一步。
- 可拦截方法:
prepare()(预编译SQL),parameterize()(设置参数),query(),update(),batch()等。 - 典型应用:
- 数据权限SQL改写: 在
prepare方法中获取原始SQL,动态添加权限相关的WHERE子句。 - 多租户SQL改写: 在SQL中添加租户ID的过滤条件。
- 分页功能: 对原始SQL进行改造,添加数据库特定分页语句(如LIMIT/OFFSET)。
- SQL加密或脱敏: 在SQL发送前对敏感数据进行处理。
- 数据权限SQL改写: 在
3. ParameterHandler (参数处理器)
- 作用: 负责将Mapper方法中传入的参数设置到PreparedStatement对象中。
- 可拦截方法:
setParameters()。 - 典型应用:
- 参数加解密: 在参数绑定到SQL前对敏感参数进行加密。
- 自定义类型转换: 对特定参数类型进行特殊处理。
- 审计参数: 记录SQL的实际执行参数。
4. ResultSetHandler (结果集处理器)
- 作用: 负责将JDBC返回的ResultSet结果集映射到Java对象中。
- 可拦截方法:
handleResultSets()。 - 典型应用:
- 结果集脱敏: 对从数据库取出的敏感数据字段进行脱敏处理后再返回给应用程序。
- 枚举类型转换: 将数据库中的数值或字符串转换为Java中的枚举对象。
- 自定义类型映射: 实现更复杂的结果集到Java对象的映射逻辑。
多少:拦截器数量、执行顺序与性能考量
关于MyBatis拦截器的数量、执行顺序以及对性能的影响,有一些重要的考量。
拦截器数量
MyBatis允许你配置任意数量的拦截器。你可以根据不同的业务需求和关注点,将功能拆分到不同的拦截器中,以保持代码的清晰和模块化。
拦截器执行顺序
当有多个拦截器配置时,它们的执行顺序非常重要。MyBatis拦截器的执行顺序是按照它们在MyBatis配置文件(通常是mybatis-config.xml)中<plugins>标签下的声明顺序决定的。
例如,如果你的配置文件中是这样声明的:
<plugins>
<plugin interceptor="com.example.interceptor.InterceptorA"/>
<plugin interceptor="com.example.interceptor.InterceptorB"/>
</plugins>
那么,当一个方法被拦截时,InterceptorA会先于InterceptorB执行其intercept()方法。这意味着InterceptorA将先对目标对象进行包装,然后InterceptorB再对InterceptorA包装后的对象进行包装。在执行链中,最后包装的拦截器(InterceptorB)会最先执行其intercept()方法的逻辑(因为它在外层),而最先包装的拦截器(InterceptorA)会在所有后续拦截器执行完毕后,或者在调用invocation.proceed()之前执行其前置逻辑,并在invocation.proceed()之后执行其后置逻辑。
性能影响
MyBatis拦截器是基于动态代理实现的,这意味着每次被拦截的方法调用都会涉及到代理对象的创建和方法调用的转发。通常情况下,这种性能开销是微乎其微的,对于大多数应用而言可以忽略不计。然而,如果拦截器内部执行了非常复杂的、耗时的操作(例如:大量的IO操作、复杂的算法计算、远程服务调用等),那么它就可能成为性能瓶颈。
性能考量建议:
- 保持简洁: 拦截器中的逻辑应尽可能简洁高效,避免不必要的复杂计算。
- 避免重复操作: 确保拦截器中的操作是必要的,并且不会与MyBatis自身的功能冲突或重复。
- 日志记录级别: 如果是性能监控或日志记录,应注意日志的级别和频率,避免产生过多的日志写入开销。
- 异步处理: 对于一些非核心、可以异步完成的任务(如审计日志发送到消息队列),可以考虑在拦截器中启动新的线程或提交到线程池处理,避免阻塞主流程。
如何:实现与配置一个MyBatis拦截器
下面我们将通过一个简单的示例来演示如何实现一个MyBatis拦截器,并将其配置到MyBatis中,以实现SQL执行时间的日志记录功能。
Step 1: 创建拦截器类
首先,创建一个Java类,实现org.apache.ibatis.plugin.Interceptor接口,并使用@Signature注解声明要拦截的方法。
package com.example.interceptor;import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 执行原始方法
long end = System.currentTimeMillis();
long cost = end - start;MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
System.out.println("SQL ID: " + sqlId + ", 执行耗时: " + cost + "ms");if (properties != null) {
String threshold = properties.getProperty("threshold");
if (threshold != null && cost > Long.parseLong(threshold)) {
System.out.println("警告: SQL ID: " + sqlId + " 执行耗时超过阈值(" + threshold + "ms)!");
}
}
return result;
}@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 使用MyBatis提供的工具类包装目标对象
}@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
在上述代码中:
@Intercepts和@Signature声明了PerformanceInterceptor将拦截Executor接口的update和query方法。- 在
intercept方法中,我们记录了方法执行前后的时间,计算耗时,并打印出来。invocation.proceed()调用了原始被拦截的方法。 plugin方法是MyBatis生成代理对象的入口,通常直接使用Plugin.wrap(target, this)即可。setProperties方法用于接收配置文件中为拦截器设置的属性。
Step 2: 配置MyBatis拦截器
将实现好的拦截器配置到MyBatis的配置文件(mybatis-config.xml)中。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
...其他配置...
<plugins>
<plugin interceptor="com.example.interceptor.PerformanceInterceptor">
<property name="threshold" value="100"/> <!-- 传递属性给拦截器 -->
</plugin>
</plugins>
...其他配置...
</configuration>
在<plugins>标签内部,通过<plugin>标签指定拦截器的完整类名。如果拦截器需要接收属性,可以在<plugin>标签内部使用<property>标签进行配置,这些属性将通过setProperties()方法传递给拦截器。
Step 3: 运行应用
当应用程序启动并执行MyBatis相关的数据库操作时,PerformanceInterceptor就会被激活,并在控制台打印出SQL的执行耗时信息。
怎么:高级技巧与注意事项
掌握了拦截器的基本实现和配置,接下来是一些高级技巧和在使用中需要注意的事项。
获取SQL语句和参数
在拦截器中获取完整的SQL语句(包括参数值)是常见的需求,但MyBatis默认的MappedStatement对象只包含带有占位符的SQL。要获取完整的SQL,需要结合MyBatis的BoundSql对象以及参数对象进行解析。
在StatementHandler的prepare方法中,可以比较容易地获取到SQL:
// 假设在StatementHandler的intercept方法中
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql(); // 原始SQL语句,带有?占位符
Object parameterObject = boundSql.getParameterObject(); // 参数对象// 如果要获取带参数值的完整SQL,需要手动解析,例如:
// Configuration configuration = mappedStatement.getConfiguration();
// TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 遍历parameterMappings和parameterObject,替换占位符... (此过程较为复杂,通常有工具类辅助)
对于生产环境,推荐使用MyBatis-Plus等框架提供的内置SQL解析工具,或者自行实现一个相对健壮的SQL解析器,以避免直接字符串替换可能导致的问题。
条件拦截
有时我们不希望拦截所有的MyBatis操作,而是只对特定的Mapper接口、方法或者SQL ID进行拦截。这可以在拦截器的intercept()方法内部通过判断MappedStatement的ID或其他信息来实现。
// 在intercept方法内部
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
if (sqlId.startsWith("com.example.mapper.UserMapper.insert")) {
// 只拦截UserMapper的insert方法
System.out.println("拦截到UserMapper的插入操作!");
return invocation.proceed();
} else {
// 不做任何处理,直接放行
return invocation.proceed();
}
异常处理
在拦截器中执行自定义逻辑时,务必考虑异常处理。如果拦截器内部抛出未捕获的异常,可能会中断MyBatis的正常执行流程,导致数据库操作失败。建议在intercept()方法中使用try-catch块来包裹自定义逻辑,确保即使发生异常,也能通过invocation.proceed()继续执行原始方法,或者优雅地处理异常。
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = null;
try {
// 自定义逻辑
System.out.println("在方法执行前执行自定义逻辑...");
result = invocation.proceed(); // 执行原始方法
System.out.println("在方法执行后执行自定义逻辑...");
} catch (Exception e) {
System.err.println("拦截器中发生异常: " + e.getMessage());
throw e; // 将异常重新抛出,让MyBatis或上层业务逻辑处理
}
return result;
}
与其他框架的集成
在Spring Boot等框架中,MyBatis通常通过SqlSessionFactoryBean进行配置。拦截器可以通过SqlSessionFactoryBean的setPlugins()方法进行设置。
@Configuration
public class MyBatisConfig {@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 设置MyBatis配置文件的路径,如果使用JavaConfig则省略
// factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));// 添加拦截器
Interceptor[] plugins = {new PerformanceInterceptor()};
factoryBean.setPlugins(plugins);return factoryBean.getObject();
}
}
总结来说,MyBatis拦截器是MyBatis生态中一个非常强大且灵活的扩展点。通过深入理解其工作原理、可拦截组件、应用场景和实现方式,我们可以有效地解决许多跨领域的技术难题,提升应用的健壮性、可维护性和扩展性。正确地使用拦截器能够让你的MyBatis应用如虎添翼,处理更为复杂的业务需求和系统级功能。