深入解析:手写MyBatis第100弹:注入黑科技:手动制造异常+强制返回值,深度测试MyBatis重试与插件容错机制

故障注入测试深度实战:模拟异常与强制返回的调试艺术

「IDEA故障注入黑科技:手动制造异常+强制返回值,深度测试MyBatis重试与插件容错机制」

故障注入:主动测试的艺术

在复杂的分布式系统中,故障是不可避免的。传统的被动调试方法往往在问题发生后才进行排查,而故障注入测试则采用主动出击的策略,在受控环境中模拟各种异常场景,提前发现和修复潜在问题。这种"破坏性"的测试方法,对于构建健壮的MyBatis应用至关重要。

  (❁´◡`❁)您的点赞➕评论➕收藏⭐是作者创作的最大动力

支持我:点赞+收藏⭐️+留言欢迎留言讨论

(源码 + 调试运行 + 问题答疑)

 有兴趣可以联系我。文末有免费源码

免费获取源码。

更多内容敬请期待。如有需要可以联系作者免费送

更多源码定制,项目修改,项目二开可以联系作者
点击可以进行搜索(每人免费送一套代码):千套源码目录(点我)

2025元旦源码免费送(点我)

我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。

手动抛出异常:深度测试异常处理机制

模拟数据库连接超时

数据库连接超时是生产环境中常见的故障场景,通过手动抛出异常可以系统性地测试MyBatis的重试和容错机制:

 public class ConnectionTimeoutSimulator {
     @Test
     public void testConnectionTimeoutHandling() {
         SqlSession sqlSession = sqlSessionFactory.openSession();
         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
         try {
             // 在调试器中手动抛出SQLTimeoutException
             User user = mapper.findById(1L);
             fail("Should have thrown SQLTimeoutException");
         } catch (Exception e) {
             // 验证异常处理逻辑
             assertTrue(e.getCause() instanceof SQLTimeoutException);
             assertEquals("Connection timeout simulated", e.getCause().getMessage());
             // 测试重试机制
             verify(retryTemplate, times(3)).execute(any(RetryCallback.class));
         }
     }
 }
 ​
 // 在MyBatis执行关键点设置调试断点
 public class SimpleExecutor extends BaseExecutor {
     @Override
     public  List doQuery(MappedStatement ms, Object parameter,
                               RowBounds rowBounds, ResultHandler resultHandler,
                               BoundSql boundSql) throws SQLException {
         Connection connection = null;
         try {
             connection = getConnection(ms.getStatementLog());
             // 调试点:在这里手动抛出SQLTimeoutException
             // 右键点击断点 → "More" → 勾选 "Suspend: Thread"
             // 在 "Evaluate and log" 中输入:
             // throw new SQLTimeoutException("Connection timeout simulated");
             PreparedStatement stmt = prepareStatement(connection, ms, boundSql);
             return query(stmt, resultHandler);
         } finally {
             closeStatement(stmt);
         }
     }
     private Connection getConnection(Log statementLog) throws SQLException {
         // 调试点:模拟连接获取超时
         Connection conn = transaction.getConnection();
         // 手动异常注入点:
         // if (System.currentTimeMillis() % 2 == 0) {
         //     throw new SQLTimeoutException("Connection acquisition timeout");
         // }
         if (statementLog.isDebugEnabled()) {
             return ConnectionLogger.newInstance(conn, statementLog, queryStack);
         } else {
             return conn;
         }
     }
 }
测试MyBatis重试机制

通过异常注入验证重试策略的有效性:

 public class RetryMechanismTester {
     private int simulatedFailureCount = 0;
     @Test
     public void testRetryOnTransientFailures() {
         UserMapper mapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
         // 模拟瞬时故障场景
         for (int i = 0; i < 5; i++) {
             try {
                 User user = mapper.findById(1L);
                 System.out.println("Attempt " + (i + 1) + " succeeded");
                 break;
             } catch (Exception e) {
                 System.out.println("Attempt " + (i + 1) + " failed: " + e.getMessage());
                 if (i == 4) {
                     fail("All retry attempts failed");
                 }
             }
         }
     }
     // 在调试器中模拟瞬时故障
     public User simulateTransientFailure(Long id) {
         // 调试点:前两次调用失败,第三次成功
         simulatedFailureCount++;
         if (simulatedFailureCount <= 2) {
             // 手动抛出异常:new SQLTransientException("Database temporarily unavailable")
             throw new SQLTransientException("Database temporarily unavailable");
         }
         return new User(id, "RecoveredUser", "recovered@example.com");
     }
 }

强制修改方法返回值:测试边界场景

验证插件链的容错性

MyBatis的插件机制基于责任链模式,通过强制修改返回值可以测试插件在各种场景下的行为:

 public class PluginChainTester {
     @Test
     public void testPluginChainWithModifiedReturn() {
         SqlSession sqlSession = sqlSessionFactory.openSession();
         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
         // 测试正常流程
         User normalUser = mapper.findById(1L);
         assertNotNull(normalUser);
         // 在调试器中强制返回null,测试空值处理
         User nullUser = mapper.findById(999L); // 不存在的ID
         // 调试点:在方法返回时强制设置为null
         // 右键断点 → "More" → 在 "Return" 中输入 "null"
         assertNull("Should return null for non-existent user", nullUser);
     }
 }
 ​
 // 测试拦截器对修改返回值的处理
 @Intercepts({
     @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
 })
 public class LoggingInterceptor implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
         try {
             Object result = invocation.proceed();
             // 调试点:强制修改查询结果
             // 在result变量上设置观察点,手动修改为测试值
             if (result instanceof List) {
                 List resultList = (List) result;
                 if (resultList.isEmpty()) {
                     // 手动注入测试数据
                     // result = Arrays.asList(new User(-1L, "TestUser", "test@example.com"));
                 }
             }
             return result;
         } catch (Exception e) {
             // 测试异常处理插件
             System.err.println("Interceptor caught exception: " + e.getMessage());
             throw e;
         }
     }
 }
边界值测试场景

通过强制返回值测试各种边界条件:

 public class BoundaryValueTester {
     public void testExtremeScenarios() {
         SqlSession sqlSession = sqlSessionFactory.openSession();
         ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
         // 场景1:空结果集处理
         List emptyList = mapper.findByCategory("NonExistentCategory");
         // 调试点:强制返回空列表
         // 在return语句处设置断点,修改返回值为 "Collections.emptyList()"
         assertTrue(emptyList.isEmpty());
         // 场景2:超大结果集
         List largeList = mapper.findByCategory("PopularCategory");
         // 调试点:模拟返回10000条记录
         // 修改返回值创建大型列表测试内存和分页
         assertEquals(10000, largeList.size());
         // 场景3:特殊值测试
         Product specialProduct = mapper.findById(0L); // 边界ID值
         // 调试点:强制返回包含特殊字符的对象
         // 修改product名称包含特殊字符: "Test\u0000Product"
         assertNotNull(specialProduct);
     }
 }

故障注入在连接池测试中的应用

模拟连接池耗尽场景

测试连接池在极端情况下的行为:

 public class ConnectionPoolStressTester {
     @Test
     public void testConnectionPoolExhaustion() throws InterruptedException {
         DataSource dataSource = getDataSource();
         int poolSize = getPoolMaxSize(dataSource);
         List connections = new ArrayList<>();
         // 占用所有连接
         for (int i = 0; i < poolSize; i++) {
             connections.add(dataSource.getConnection());
         }
         // 测试获取额外连接的场景
         ExecutorService executor = Executors.newSingleThreadExecutor();
         Future future = executor.submit(() -> {
             // 这个调用应该阻塞或抛出异常
             return dataSource.getConnection();
         });
         try {
             Connection extraConn = future.get(2, TimeUnit.SECONDS);
             // 如果到达这里,说明连接池行为异常
             fail("Should not acquire extra connection");
         } catch (TimeoutException e) {
             // 期望的行为:获取连接超时
             System.out.println("Correctly timed out waiting for connection");
         } finally {
             // 清理连接
             connections.forEach(conn -> {
                 try { conn.close(); } catch (SQLException e) { /* ignore */ }
             });
             executor.shutdown();
         }
     }
     // 在连接获取方法中注入故障
     public class FaultInjectingDataSource implements DataSource {
         private final DataSource realDataSource;
         private boolean injectFailure = false;
         @Override
         public Connection getConnection() throws SQLException {
             // 调试点:手动控制故障注入
             if (injectFailure) {
                 throw new SQLTransientConnectionException("Connection pool exhausted");
             }
             return realDataSource.getConnection();
         }
         public void setInjectFailure(boolean injectFailure) {
             this.injectFailure = injectFailure;
         }
     }
 }

事务管理器的故障注入测试

模拟事务回滚场景

测试事务在各种故障场景下的回滚行为:

 public class TransactionFailureTester {
     @Test
     public void testTransactionRollbackOnFailure() {
         SqlSession sqlSession = sqlSessionFactory.openSession();
         UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
         OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
         try {
             // 开始事务
             User user = new User("TestUser", "test@example.com");
             userMapper.insert(user);
             // 调试点:在订单创建前手动抛出异常
             // 设置断点并抛出 RuntimeException("Simulated business failure")
             Order order = new Order(user.getId(), BigDecimal.valueOf(100.0));
             orderMapper.insert(order);
             sqlSession.commit();
             fail("Transaction should have rolled back");
         } catch (Exception e) {
             // 验证事务回滚
             sqlSession.rollback();
             assertTrue(e.getMessage().contains("Simulated business failure"));
             // 验证数据一致性:用户记录应该不存在
             SqlSession newSession = sqlSessionFactory.openSession();
             User insertedUser = newSession.getMapper(UserMapper.class).findByName("TestUser");
             assertNull("User should not be inserted due to rollback", insertedUser);
             newSession.close();
         } finally {
             sqlSession.close();
         }
     }
 }

缓存系统的故障注入

测试缓存一致性问题

模拟缓存失效、脏读等场景:

 public class CacheFailureTester {
     @Test
     public void testCacheConsistencyUnderFailure() {
         SqlSession sqlSession1 = sqlSessionFactory.openSession();
         SqlSession sqlSession2 = sqlSessionFactory.openSession();
         UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
         UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
         // 会话1查询并缓存数据
         User user1 = mapper1.findById(1L);
         assertNotNull(user1);
         // 模拟缓存失效:在调试器中手动清除缓存
         // 在 Cache 的 getObject 方法中设置断点,强制返回 null
         sqlSession1.clearCache();
         // 会话2修改数据
         user1.setEmail("updated@example.com");
         mapper2.updateUser(user1);
         sqlSession2.commit();
         // 测试会话1的缓存是否正确处理了失效
         User cachedUser = mapper1.findById(1L);
         // 调试点:验证缓存重新加载逻辑
         assertEquals("updated@example.com", cachedUser.getEmail());
         sqlSession1.close();
         sqlSession2.close();
     }
     // 测试二级缓存故障场景
     public void testSecondLevelCacheFailure() {
         // 模拟分布式缓存故障
         Cache secondLevelCache = configuration.getCache("com.example.UserMapper");
         // 调试点:手动使缓存失效
         secondLevelCache.clear();
         // 测试缓存重新构建过程
         SqlSession sqlSession = sqlSessionFactory.openSession();
         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
         long startTime = System.currentTimeMillis();
         User user = mapper.findById(1L); // 应该触发缓存重建
         long duration = System.currentTimeMillis() - startTime;
         // 验证缓存重建后的性能
         assertTrue("Cache should be rebuilt", duration < 1000);
         sqlSession.close();
     }
 }

故障注入测试的最佳实践

系统化的测试策略

建立完整的故障注入测试体系:

  1. 故障分类

    • 瞬时故障(网络抖动、数据库暂时不可用)

    • 持久故障(数据库连接失败、磁盘空间不足)

    • 逻辑故障(业务规则违反、数据一致性問題)

  2. 注入时机

    • 启动阶段(应用初始化时)

    • 运行时(业务处理过程中)

    • 关闭阶段(应用关闭时)

  3. 影响范围

    • 组件级别(单个Mapper或Service)

    • 服务级别(整个业务功能)

    • 系统级别(影响多个服务)

自动化故障注入框架

构建自动化的故障注入测试框架:

 public class AutomatedFaultInjection {
     private static final Map rules = new ConcurrentHashMap<>();
     public static void registerFaultRule(String pointcut, FaultInjectionRule rule) {
         rules.put(pointcut, rule);
     }
     public static Object injectFault(String pointcut, Object originalResult) {
         FaultInjectionRule rule = rules.get(pointcut);
         if (rule != null && rule.shouldInject()) {
             if (rule.shouldThrowException()) {
                 throw rule.getExceptionToThrow();
             } else {
                 return rule.getModifiedResult();
             }
         }
         return originalResult;
     }
     // 在MyBatis关键点插入故障注入
     public class FaultAwareExecutor extends SimpleExecutor {
         @Override
         public  List doQuery(MappedStatement ms, Object parameter,
                                   RowBounds rowBounds, ResultHandler resultHandler,
                                   BoundSql boundSql) throws SQLException {
             // 故障注入点
             String pointcut = "executor.query:" + ms.getId();
             try {
                 List result = super.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
                 return (List) injectFault(pointcut, result);
             } catch (RuntimeException e) {
                 if (e.getCause() instanceof SQLException) {
                     throw (SQLException) e.getCause();
                 }
                 throw e;
             }
         }
     }
 }

生产环境故障注入的谨慎应用

在生产环境进行故障注入需要极其谨慎的策略:

public class ProductionFaultInjection {
    private static final double INJECTION_PROBABILITY = 0.001; // 0.1%的概率
    public static boolean shouldInjectFault() {
        // 只在极小概率下注入故障,避免影响正常用户
        return Math.random() < INJECTION_PROBABILITY;
    }
    public static void safeFaultInjection(String scenario) {
        if (shouldInjectFault()) {
            log.warn("Injecting controlled fault for scenario: {}", scenario);
            // 执行受控的故障注入
            injectControlledFault(scenario);
        }
    }
    private static void injectControlledFault(String scenario) {
        // 在生产环境中只注入可安全处理的故障
        switch (scenario) {
            case "TIMEOUT":
                // 模拟轻微延迟,不模拟完全失败
                try { Thread.sleep(100); } catch (InterruptedException e) { /* ignore */ }
                break;
            case "CACHE_MISS":
                // 只清除特定缓存条目
                clearSpecificCacheEntries();
                break;
            default:
                // 生产环境不模拟严重故障
                log.debug("Skipping severe fault injection in production");
        }
    }
}

总结

故障注入测试是一种强大的质量保障手段,它通过主动引入故障来验证系统的容错能力和恢复机制。在MyBatis应用开发中,通过手动抛出异常和强制修改返回值,我们可以:

  1. 验证异常处理:确保系统能够优雅地处理各种故障场景

  2. 测试重试机制:验证重试策略的有效性和正确性

  3. 检查边界条件:测试系统在各种极端情况下的行为

  4. 提升系统韧性:通过提前发现问题来增强系统的稳定性

掌握故障注入测试技巧,不仅能够提升代码质量,更能培养预防性编程的思维方式,这是在构建高可用分布式系统中不可或缺的核心能力。


(❁´◡`❁)您的点赞➕评论➕收藏⭐是作者创作的最大动力

支持我:点赞+收藏⭐️+留言欢迎留言讨论

(源码 + 调试运行 + 问题答疑)

 有兴趣可以联系我。文末有免费源码

学习知识需费心,
整理归纳更费神。
源码免费人人喜,
码农福利等你领!

常来我家多看看,
网址:扣棣编程
感谢支持常陪伴,
点赞关注别忘记!

山高路远坑又深,
大军纵横任驰奔,
谁敢横刀立马行?
唯有点赞+关注成!

往期文章推荐:

基于Springboot + vue实现的学生宿舍信息管理系统
免费获取宠物商城源码--SpringBoot+Vue宠物商城网站系统
【2025小年源码免费送】

⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇

posted on 2025-11-05 14:54  wgwyanfs  阅读(8)  评论(0)    收藏  举报

导航