第10章 插件开发
学习 MyBatis 插件的开发和拦截器的使用
学习进度:40% 代码演示按钮 💻 查看完整代码 - 在线IDE体验
🎯 学习目标
📚
理解核心概念
掌握本章的核心概念和基本原理
🔧
掌握实践技能
学会相关的配置方法和实践技巧
💡
了解应用场景
明确在实际项目中的应用场景和最佳实践
学习目标
- 深入理解MyBatis插件机制的工作原理
- 掌握拦截器的开发和配置方法
- 学会开发常用的功能插件
- 了解插件的执行顺序和生命周期
- 掌握插件性能优化技巧
- 学会调试和测试插件功能
插件机制概述
MyBatis插件机制基于JDK动态代理和责任链模式,允许开发者在SQL执行的关键节点进行拦截和增强。通过插件,可以实现性能监控、SQL审计、分页查询、缓存增强等功能。
插件的优势
- 非侵入性:不需要修改原有代码
- 可插拔:可以灵活地启用或禁用功能
- 可扩展:支持自定义功能扩展
- 链式处理:多个插件可以协同工作
可拦截的对象
MyBatis允许拦截以下四种对象的方法调用:
- Executor:执行器,负责SQL的执行和缓存维护
- ParameterHandler:参数处理器,负责参数的设置
- ResultSetHandler:结果集处理器,负责结果的映射
- StatementHandler:语句处理器,负责SQL语句的准备和执行
插件开发基础
实现Interceptor接口
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
.main-content {
padding-right: 15px;
}
.main-content .container {
padding-left: 0;
padding-right: 0;
}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PerformanceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行原方法
Object result = invocation.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录执行时间
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
System.out.println("SQL执行时间: " + ms.getId() + " -> " + duration + "ms");
}
}
@Override
public Object plugin(Object target) {
// 使用Plugin.wrap方法创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 设置插件属性
String threshold = properties.getProperty("threshold", "100");
System.out.println("性能监控阈值: " + threshold + "ms");
}
}
注解说明
- @Intercepts:标记这是一个拦截器
- @Signature:指定要拦截的方法签名
- type:要拦截的对象类型
- method:要拦截的方法名
- args:方法的参数类型数组
插件注册
<!-- 在mybatis-config.xml中注册插件 -->
<configuration>
<plugins>
<!-- 性能监控插件 -->
<plugin interceptor="com.example.mybatis.plugin.PerformanceInterceptor">
<property name="threshold" value="100"/>
</plugin>
<!-- SQL日志插件 -->
<plugin interceptor="com.example.mybatis.plugin.SqlLogInterceptor">
<property name="logLevel" value="DEBUG"/>
</plugin>
<!-- 分页插件 -->
<plugin interceptor="com.example.mybatis.plugin.PaginationInterceptor"/>
</plugins>
</configuration>
常用插件开发实例
1. SQL日志插件
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SqlLogInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlLogInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
// 获取SQL语句
String sql = boundSql.getSql();
// 获取参数
Object parameterObject = boundSql.getParameterObject();
// 格式化SQL
String formattedSql = formatSql(sql);
// 记录SQL日志
logger.info("执行SQL: {}", formattedSql);
logger.info("参数: {}", parameterObject);
return invocation.proceed();
}
private String formatSql(String sql) {
// 简单的SQL格式化
return sql.replaceAll("\s+", " ").trim();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以配置日志级别等属性
}
}
2. 分页插件
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PaginationInterceptor implements Interceptor {
@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 == null || rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
}
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameter);
// 构建count查询
String countSql = buildCountSql(boundSql.getSql());
// 执行count查询
Long total = executeCountQuery(executor, ms, parameter, countSql);
// 构建分页查询
String pageSql = buildPageSql(boundSql.getSql(), rowBounds);
// 创建新的MappedStatement
MappedStatement newMs = copyMappedStatement(ms, new BoundSqlSource(boundSql, pageSql));
args[0] = newMs;
args[2] = RowBounds.DEFAULT;
// 执行分页查询
List result = (List) invocation.proceed();
// 封装分页结果
return new PageResult<>(result, total, rowBounds.getOffset(), rowBounds.getLimit());
}
private String buildCountSql(String originalSql) {
return "SELECT COUNT(*) FROM (" + originalSql + ") tmp_count";
}
private String buildPageSql(String originalSql, RowBounds rowBounds) {
return originalSql + " LIMIT " + rowBounds.getOffset() + ", " + rowBounds.getLimit();
}
private Long executeCountQuery(Executor executor, MappedStatement ms,
Object parameter, String countSql) throws SQLException {
// 实现count查询逻辑
// ...
return 0L;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 配置数据库方言等
}
}
3. SQL审计插件
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class SqlAuditInterceptor implements Interceptor {
private static final Logger auditLogger = LoggerFactory.getLogger("SQL_AUDIT");
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 记录操作前信息
AuditInfo auditInfo = new AuditInfo();
auditInfo.setStatementId(ms.getId());
auditInfo.setSqlCommandType(ms.getSqlCommandType().name());
auditInfo.setStartTime(new Date(startTime));
auditInfo.setUserId(getCurrentUserId());
auditInfo.setParameter(parameter);
try {
// 执行SQL
Object result = invocation.proceed();
// 记录成功信息
auditInfo.setSuccess(true);
auditInfo.setAffectedRows(getAffectedRows(result));
return result;
} catch (Exception e) {
// 记录异常信息
auditInfo.setSuccess(false);
auditInfo.setErrorMessage(e.getMessage());
throw e;
} finally {
// 记录结束时间和耗时
long endTime = System.currentTimeMillis();
auditInfo.setEndTime(new Date(endTime));
auditInfo.setDuration(endTime - startTime);
// 输出审计日志
auditLogger.info("SQL审计: {}", JSON.toJSONString(auditInfo));
}
}
private String getCurrentUserId() {
// 获取当前用户ID,可以从ThreadLocal或Security Context中获取
return "system";
}
private int getAffectedRows(Object result) {
if (result instanceof Integer) {
return (Integer) result;
}
return 0;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 配置审计相关属性
}
// 审计信息实体类
public static class AuditInfo {
private String statementId;
private String sqlCommandType;
private Date startTime;
private Date endTime;
private long duration;
private String userId;
private Object parameter;
private boolean success;
private int affectedRows;
private String errorMessage;
// getter和setter方法
}
}
高级插件开发技巧
1. 插件链和执行顺序
多个插件会形成一个拦截器链,执行顺序与注册顺序相反(后注册的先执行)。
// 插件执行顺序示例
// 注册顺序:Plugin1 -> Plugin2 -> Plugin3
// 执行顺序:Plugin3 -> Plugin2 -> Plugin1 -> 目标方法 -> Plugin1 -> Plugin2 -> Plugin3
public class PluginChainDemo {
public static void demonstratePluginChain() {
// Plugin3.intercept() 开始
// Plugin2.intercept() 开始
// Plugin1.intercept() 开始
// 目标方法执行
// Plugin1.intercept() 结束
// Plugin2.intercept() 结束
// Plugin3.intercept() 结束
}
}
2. 条件拦截
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class ConditionalInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// 只拦截特定的Mapper方法
if (shouldIntercept(ms)) {
// 执行拦截逻辑
return doIntercept(invocation);
}
// 不拦截,直接执行原方法
return invocation.proceed();
}
private boolean shouldIntercept(MappedStatement ms) {
String id = ms.getId();
// 只拦截UserMapper的方法
return id.contains("UserMapper");
}
private Object doIntercept(Invocation invocation) throws Throwable {
// 具体的拦截逻辑
System.out.println("拦截UserMapper方法");
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 配置属性
}
}
3. 参数和结果处理
@Intercepts({
@Signature(
type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class}
)
})
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对象
Field field = parameterHandler.getClass().getDeclaredField("parameterObject");
field.setAccessible(true);
Object parameterObject = field.get(parameterHandler);
// 处理参数(例如:敏感信息加密)
if (parameterObject instanceof User) {
User user = (User) parameterObject;
if (user.getPassword() != null) {
user.setPassword(encrypt(user.getPassword()));
}
}
return invocation.proceed();
}
private String encrypt(String password) {
// 简单的加密示例
return "encrypted_" + password;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 配置加密算法等
}
}
插件配置和管理
1. 外部配置文件
# plugin-config.properties
# 性能监控配置
performance.threshold=100
performance.slowSqlLog=true
# SQL日志配置
sqllog.level=DEBUG
sqllog.format=pretty
# 分页配置
pagination.dialect=mysql
pagination.reasonable=true
# 审计配置
audit.enabled=true
audit.logFile=/var/log/mybatis-audit.log
2. 动态配置加载
public class ConfigurableInterceptor implements Interceptor {
private Properties config;
@Override
public void setProperties(Properties properties) {
this.config = new Properties();
// 加载默认配置
loadDefaultConfig();
// 加载外部配置文件
loadExternalConfig();
// 应用传入的配置(优先级最高)
this.config.putAll(properties);
}
private void loadDefaultConfig() {
config.setProperty("threshold", "100");
config.setProperty("enabled", "true");
}
private void loadExternalConfig() {
try {
InputStream is = getClass().getClassLoader()
.getResourceAsStream("plugin-config.properties");
if (is != null) {
config.load(is);
}
} catch (IOException e) {
// 处理异常
}
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 使用配置
boolean enabled = Boolean.parseBoolean(config.getProperty("enabled", "true"));
if (!enabled) {
return invocation.proceed();
}
// 其他逻辑
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
插件性能优化
1. 减少反射调用
- 缓存反射获取的Field和Method
- 使用高性能的反射库(如ReflectASM)
- 避免在热点路径中使用反射
2. 优化字符串操作
- 使用StringBuilder进行字符串拼接
- 缓存格式化后的SQL语句
- 避免不必要的字符串创建
3. 异步处理
- 将耗时操作(如日志写入)异步化
- 使用线程池处理后台任务
- 避免阻塞主执行流程
4. 内存管理
- 及时释放大对象引用
- 使用对象池减少GC压力
- 监控内存使用情况
插件调试和测试
1. 调试技巧
public class DebugInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(DebugInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 打印调用信息
Object target = invocation.getTarget();
Method method = invocation.getMethod();
Object[] args = invocation.getArgs();
logger.debug("拦截调用: {}.{}({})",
target.getClass().getSimpleName(),
method.getName(),
Arrays.toString(args));
// 打印调用栈
if (logger.isTraceEnabled()) {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (int i = 0; i < Math.min(stack.length, 10); i++) {
logger.trace(" at {}", stack[i]);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 配置调试级别
}
}
2. 单元测试
public class PerformanceInterceptorTest {
private PerformanceInterceptor interceptor;
private Invocation mockInvocation;
@Before
public void setUp() {
interceptor = new PerformanceInterceptor();
// 设置属性
Properties props = new Properties();
props.setProperty("threshold", "50");
interceptor.setProperties(props);
// 创建模拟对象
mockInvocation = mock(Invocation.class);
}
@Test
public void testIntercept() throws Throwable {
// 模拟方法调用
when(mockInvocation.proceed()).thenReturn("result");
MappedStatement ms = mock(MappedStatement.class);
when(ms.getId()).thenReturn("com.example.UserMapper.selectById");
when(mockInvocation.getArgs()).thenReturn(new Object[]{ms, 1L});
// 执行拦截
Object result = interceptor.intercept(mockInvocation);
// 验证结果
assertEquals("result", result);
verify(mockInvocation).proceed();
}
@Test
public void testPlugin() {
Object target = new Object();
Object plugin = interceptor.plugin(target);
// 验证返回的是代理对象
assertNotSame(target, plugin);
assertTrue(Proxy.isProxyClass(plugin.getClass()));
}
}
常见问题与解决方案
Q: 插件不生效怎么办?
A: 检查插件注册配置、@Signature注解的方法签名是否正确、plugin方法是否正确返回代理对象。
Q: 插件执行顺序如何控制?
A: 插件执行顺序与注册顺序相反,可以通过调整配置文件中的顺序来控制。
Q: 如何避免插件影响性能?
A: 减少反射调用、异步处理耗时操作、添加开关控制、优化算法复杂度。
Q: 插件中如何获取SQL语句?
A: 通过MappedStatement.getBoundSql()方法获取BoundSql对象,再调用getSql()方法。
Q: 如何在插件中修改SQL?
A: 创建新的BoundSql对象和MappedStatement,替换Invocation中的参数。
实践练习
练习1:性能监控插件
开发一个性能监控插件,记录SQL执行时间,并在超过阈值时发出警告。
练习2:SQL美化插件
开发一个SQL格式化插件,将SQL语句格式化后输出到日志。
练习3:参数加密插件
开发一个参数处理插件,自动加密敏感字段。
练习4:结果缓存插件
开发一个结果缓存插件,缓存查询结果以提高性能。
练习5:多数据源路由插件
开发一个数据源路由插件,根据SQL类型自动选择读写数据源。
运行示例程序
步骤1:导入项目
将chapter-10-plugins.json导入到IntelliJ IDEA中。
步骤2:配置数据库
确保MySQL数据库运行正常,执行init.sql创建测试数据。
步骤3:运行基础示例
运行PluginDemo.java,观察各种插件的工作效果。
步骤4:观察插件日志
查看控制台输出,理解插件的拦截和处理过程。
步骤5:测试性能影响
对比有插件和无插件的性能差异,分析插件开销。
步骤6:自定义插件开发
尝试开发自己的插件,实现特定的业务需求。
步骤7:运行测试用例
执行PluginTest.java中的测试用例,验证插件功能的正确性。
本章小结
📖 本章小结
本章我们学习了 插件开发 的相关内容,包括:
- 核心概念和基本原理
- 配置方法和实践技巧
- 应用场景和最佳实践
- 常见问题和解决方案
下一章节 🎉
恭喜完成本章学习!
你已经掌握了 插件开发 的核心知识
第11章关联映射继续学习 MyBatis 的更多特性和应用⏱️预计 30 分钟📊中等难度开始学习 → ← 第09章 📚 课程目录 第11章 →