3141 字
约 10 分钟
14
插件开发
无标签

第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章 →

插件开发
https://clxhxhhr.top/posts/2852/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。