ContiPerf:一个轻量级Java性能测试框架,SpringBoot集成只需5分钟
做Java开发的性能测试一般想到的就是JMeter。但说实话很多场景用JMeter太重了——你得单独安装、配环境、设计测试计划、调试脚本。我就想快速测一下某个接口的吞吐量和延迟搞那么复杂干嘛今天给大家介绍一个轻量级的性能测试框架 ContiPerf基于JUnit 4在单元测试上加几个注解就能跑性能测试直接在IDE里运行零额外部署。一、ContiPerf 是什么ContiPerf 是一个基于 JUnit 4 的轻量级性能测试框架。它通过 TestRule 机制把普通的Test方法转化为可配置的多线程性能测试。不需要启动独立的压测工具不需要额外的Agent进程跑个单元测试的功夫就把性能数据拿到了。核心特性特性说明注解驱动PerfTest、Required 等注解定义测试参数零XML配置并发控制支持配置线程数、总调用次数、执行时长预热支持WarmUp 注解实现JIT预热避免冷启动偏差阈值校验Required 定义吞吐量、延迟等性能基线不达标自动失败分位值统计自动计算 Min/Max/Avg/Median/P90/P95/P99多种报告控制台输出 CSV文件 HTML报告SpringBoot集成配合SpringBootTest直接测试Spring Bean灵活调度支持渐增线程rampUp、虚拟时钟、超时控制开源地址https://github.com/literarni/contiperf 环境要求JDK 1.8、JUnit 4 开源协议Apache 2.0Maven坐标dependency groupIdorg.databene/groupId artifactIdcontiperf/artifactId version2.3.4/version scopetest/scope /dependency二、核心概念2.1 注解体系ContiPerf 的核心是注解驱动几个关键注解搞清楚就够了注解作用关键参数PerfTest定义性能测试参数invocations、threads、durationRequired定义性能阈值throughput、max、average、percentilesPerfTestConfig全局测试配置reportLocation、reportType、assertionConfigWarmUp预热配置invocationsWaitBefore执行前等待ms2.2 执行流程ContiPerf 把一个普通的测试方法包装成性能测试内部执行流程如下关键点预热阶段的执行结果不会计入最终统计这是为了避免JIT编译冷启动导致的延迟偏差。生产环境中我一般设置 20-50 次预热。三、SpringBoot 集成3.1 环境准备本文使用以下技术栈SpringBoot2.7.18ContiPerf2.3.4JUnit4.13.2SpringBoot 2.x 默认支持JDK1.83.2 项目依赖!-- pom.xml -- dependencies !-- SpringBoot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency !-- ContiPerf -- dependency groupIdorg.databene/groupId artifactIdcontiperf/artifactId version2.3.4/version scopetest/scope /dependency !-- SpringBoot Test自带 JUnit 4 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId version2.7.18/version scopetest/scope /dependency /dependenciesSpringBoot 2.x 同时支持 JUnit 4 和 JUnit 5但 ContiPerf 只认 JUnit 4。测试类用RunWith(SpringRunner.class)别用 JUnit 5 的ExtendWith。3.3 ContiPerfRule 基本用法ContiPerf 的核心入口是ContiPerfRule一个 JUnit TestRule加到测试类里就行import org.databene.contiperf.PerfTest; import org.databene.contiperf.junit.ContiPerfRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; RunWith(SpringRunner.class) SpringBootTest public class UserServicePerfTest { Rule public ContiPerfRule contiPerfRule new ContiPerfRule(); Autowired private UserService userService; Test PerfTest(invocations 1000, threads 10) public void testGetUserById() { userService.getUserById(1L); } }跑完后控制台直接输出结果[ContiPerf] UserServicePerfTest.testGetUserById samples: 1000 max: 45 ms average: 8 ms median: 5 ms min: 1 ms throughput: 125.3/s 90%: 12 ms 95%: 18 ms 99%: 35 ms同时会在target/contiperf-report目录下生成 HTML 报告。四、实战案例4.1 固定调用次数1000 次调用、10 个并发线程Test PerfTest(invocations 1000, threads 10) public void testListUsers() { userService.listUsers(1, 20); }4.2 持续压测不确定调用次数就按时间压持续跑 30 秒Test PerfTest(duration 30000, threads 20) public void testCreateOrder() { OrderDTO order new OrderDTO(); order.setProductId(1L); order.setQuantity(2); orderService.createOrder(order); }invocations和duration二选一不能同时用。4.3 加预热JVM 刚启动时性能不稳定JIT 编译还没生效。加个预热Test PerfTest(invocations 1000, threads 10) WarmUp(invocations 50) public void testGetProductDetail() { productService.getProductDetail(100L); }WarmUp(invocations 50)表示先跑 50 次不计入统计让 JIT 编译完成后再正式采集数据。这个在生产压测中很有用我之前遇到过不加预热测出来平均 50ms加了预热变成 5ms 的情况差距 10 倍。4.4 性能阈值校验光看数据没用得有标准。Required可以设定阈值不达标测试直接失败Test PerfTest(invocations 1000, threads 10) Required(throughput 100) Required(max 200) Required(average 50) public void testQueryOrder() { orderService.queryOrder(ORD202401010001); }吞吐量不低于 100 ops/s最大延迟不超过 200ms平均延迟不超过 50ms。任一指标不达标测试直接挂。4.5 分位值校验光看平均值不够线上一般用 P99 来衡量用户体验Test PerfTest(invocations 2000, threads 20) Required(percentiles 90:50,95:100,99:200) public void testSearchProducts() { productService.search(手机, 1, 10); }90% 的请求要在 50ms 内、95% 在 100ms 内、99% 在 200ms 内。实际项目用得最多SLA 一般也是按 P99 来定的。4.6 渐增并发Ramp Up一上来就满并发容易把服务打挂特别是有限流、熔断的场景。用rampUp逐步拉起线程Test PerfTest(invocations 5000, threads 50, rampUp 5000) public void testFlashSale() { flashSaleService.purchase(1L, user_ ThreadLocalRandom.current().nextLong(10000)); }rampUp 5000表示在 5 秒内把 50 个线程逐步拉起来而不是一上来就全部启动。这个对秒杀、抢购场景的压测特别有用。4.7 完整的综合案例一个实际项目中的订单服务测试类覆盖创建、查询、取消三个接口RunWith(SpringRunner.class) SpringBootTest PerfTestConfig(reportLocation target/contiperf-report, reportType ReportType.HTML_CSV) public class OrderServicePerfTest { Rule public ContiPerfRule contiPerfRule new ContiPerfRule(); Autowired private OrderService orderService; Test PerfTest(invocations 1000, threads 10) WarmUp(invocations 20) Required(throughput 200) Required(max 500) Required(percentiles 90:50,95:100,99:300) public void testCreateOrder() { OrderDTO dto new OrderDTO(); dto.setUserId(ThreadLocalRandom.current().nextLong(1, 10000)); dto.setProductId(ThreadLocalRandom.current().nextLong(1, 1000)); dto.setQuantity(1); orderService.createOrder(dto); } Test PerfTest(invocations 2000, threads 20) WarmUp(invocations 30) Required(throughput 500) Required(average 20) Required(percentiles 99:100) public void testQueryOrder() { orderService.queryById(ThreadLocalRandom.current().nextLong(1, 100000)); } Test PerfTest(duration 10000, threads 5) WarmUp(invocations 10) Required(throughput 100) public void testCancelOrder() { orderService.cancelOrder(ThreadLocalRandom.current().nextLong(1, 50000)); } }几个细节PerfTestConfig放类上统一配报告输出WarmUp每个方法单独配用ThreadLocalRandom生成随机参数避免缓存命中率过高导致数据失真testCancelOrder用 duration 模式持续压 10 秒。五、核心注解详解5.1 PerfTest 参数全解参数类型默认值说明invocationsint1总调用次数threadsint1并发线程数durationlong0持续时长毫秒和invocations二选一rampUplong0渐增时间毫秒线程逐步启动timeoutlong0单次调用超时毫秒0不限制maxTimelong0整个测试最大执行时间毫秒5.2 Required 参数全解参数类型说明throughputdouble最小吞吐量ops/smaxlong最大延迟毫秒averagelong平均延迟上限毫秒medianlong中位数延迟上限毫秒totalTimelong总执行时间上限毫秒percentilesString分位值要求格式 “分位:毫秒”pageSizeint报告中的分页大小percentiles的格式是逗号分隔的键值对90:30,95:50,99:100。多个Required可以叠加使用。5.3 WarmUp 参数参数类型默认值说明invocationsint1预热调用次数5.4 PerfTestConfig 参数参数类型默认Value说明reportLocationString“target/contiperf-report”报告输出路径reportTypesReportType[]{HTML}报告格式支持 HTML、CSVassertionConfigAssertionConfigWARN断言失败策略FAIL 或 WARNcalendarString“system”虚拟时钟配置六、进阶用法6.1 自定义报告输出ContiPerf 默认在target/contiperf-report下生成报告可以自定义Rule public ContiPerfRule contiPerfRule new ContiPerfRule( new FileReportModule.Builder() .outputFolder(custom-report-dir) .reportTypes(ReportType.HTML, ReportType.CSV) .build() );或者在类级别配置PerfTestConfig( reportLocation build/reports/contiperf, reportType {ReportType.HTML, ReportType.CSV} )6.2 断言失败策略ContiPerf 支持两种断言失败策略// 策略一不达标就测试失败推荐用于CI/CD PerfTestConfig(assertionConfig AssertionConfig.FAIL) // 策略二不达标只警告测试仍然通过 PerfTestConfig(assertionConfig AssertionConfig.WARN)CI/CD 流水线中用FAIL开发阶段调试用WARN这是比较务实的做法。6.3 测试带事务的Service在 SpringBoot 中测试写操作有时候需要回滚数据RunWith(SpringRunner.class) SpringBootTest Transactional public class OrderWritePerfTest { Rule public ContiPerfRule contiPerfRule new ContiPerfRule(); Autowired private OrderService orderService; Test PerfTest(invocations 500, threads 5) Rollback public void testBatchInsert() { Order order new Order(); order.setOrderNo(PERF_ System.nanoTime()); orderService.insert(order); } }加了TransactionalRollback每次测试完数据自动回滚不会污染数据库。6.4 参数化性能测试ContiPerf 和 JUnit 4 的参数化测试可以配合使用测不同参数下的性能表现RunWith(Parameterized.class) SpringBootTest PerfTestConfig(assertionConfig AssertionConfig.WARN) public class ParamPerfTest { Rule public ContiPerfRule contiPerfRule new ContiPerfRule(); Autowired private ProductService productService; Parameter(value 0) public int pageSize; Parameters(name pageSize{0}) public static CollectionObject[] data() { return Arrays.asList(new Object[][] { { 10 }, { 20 }, { 50 }, { 100 } }); } Test PerfTest(invocations 500, threads 10) public void testSearch() { productService.search(手机, 1, pageSize); } }这样能看到不同分页大小对接口性能的影响。6.5 直接用API方式非注解如果不习惯注解方式ContiPerf 也提供了编程式APITest public void testApiStyle() { PerformanceTest test new PerformanceTest(); test.setInvocations(1000); test.setThreadCount(10); test.setRampUp(2000); TestDescriptor descriptor new TestDescriptor( getClass(), testApiStyle ); PerformanceTestResult result new PerformanceTestRunner() .setTest(test) .setTestDescriptor(descriptor) .run(() - userService.getUserById(1L)); System.out.println(吞吐量: result.getThroughput()); System.out.println(平均延迟: result.getAverageLatency() ms); System.out.println(P99: result.getPercentileLatency(99) ms); }不过说实话注解方式代码量少太多除非有动态配置的需求一般用注解就够了。七、ContiPerf vs JMeter vs Gatling对比项ContiPerfJMeterGatling部署方式依赖引入即可独立安装GUI应用SBT插件/独立包使用方式注解 单元测试GUI拖拽/脚本Scala DSL学习成本低会JUnit就会用中得学GUI操作高需要会ScalaCI/CD集成天然集成就是JUnit需要额外配置需要额外配置适用场景单接口压测、回归测试全链路压测高并发场景压测分布式不支持支持支持协议支持Java方法调用HTTP/JDBC/JMS等HTTP/WebSocket报告HTML/CSVHTML/图表HTML交互式报告我的建议单接口、Service 层性能回归 → ContiPerf简单省事HTTP 接口全链路压测 → JMeter功能全面高并发、大数据量场景 → Gatling性能更好ContiPerf 的优势在于它就是个 JUnit 测试能跑单元测试的地方就能跑性能测试。CI/CD 流水线里加个阶段就行不需要额外维护压测脚本和环境。八、踩坑记录8.1 JUnit 5 兼容问题ContiPerf 只支持 JUnit 4SpringBoot 2.2 默认用 JUnit 5。需要在pom.xml里排除 JUnit 5dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId version2.7.18/version scopetest/scope exclusions !-- 排除 JUnit 5使用 JUnit 4 -- exclusion groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId /exclusion exclusion groupIdorg.junit.vintage/groupId artifactIdjunit-vintage-engine/artifactId /exclusion /exclusions /dependency !-- 手动引入 JUnit 4 -- dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency测试类用RunWith(SpringRunner.class)别用ExtendWith。8.2 数据库连接池不够ContiPerf 默认线程数是 1很多人一上来就设 threads 100结果数据库连接池爆了。线程数要和数据库连接池大小匹配。比如 HikariCP 默认最大连接数 10你测 threads 50 的写操作一大半线程都在等连接。经验值读操作线程数可以大于连接池查询快释放快写操作线程数别超过连接池的 1.5 倍。8.3 多线程下 Transactional 失效前面说了用TransactionalRollback回滚数据。但多线程下这玩意会失效——Spring 事务绑定在 ThreadLocal 上只有主线程有事务子线程没有。这个坑我在线上跑了一周才发现。解决办法如果需要在并发测试中保证事务用编程式事务替代注解Autowired private TransactionTemplate transactionTemplate; Test PerfTest(invocations 500, threads 10) public void testTransactionalWrite() { transactionTemplate.execute(status - { orderService.insert(buildOrder()); return null; }); }8.4 Spring 上下文缓存SpringBootTest启动一次 Spring 上下文慢得要命。多个性能测试类的话抽个基类避免重复启动RunWith(SpringRunner.class) SpringBootTest PerfTestConfig(reportLocation target/contiperf-report) public abstract class BasePerfTest { Rule public ContiPerfRule contiPerfRule new ContiPerfRule(); }然后所有测试类继承这个基类就行Spring 上下文只启动一次。九、最佳实践预热不能省至少 20 次让 JIT 编译完成。不用预热测出来的数据基本不准。用随机参数别每次都查 id1 的数据Redis 缓存/数据库缓存会严重影响结果。线程数要合理不是越大越好先看目标环境的实际并发量。分位值比平均值有用平均值 10ms 但 P99 是 500ms用户体验还是很差。重点关注 P95 和 P99。CI/CD 中加性能回归用Required设定阈值断言策略用FAIL性能退化能立即发现。报告归档把target/contiperf-report配到 CI 的 artifacts 中方便对比不同版本的数据。十、什么时候用 ContiPerf适合Service 层方法性能基准测试、接口吞吐量和延迟回归、CI/CD 性能门禁、优化前后对比。不适合大规模分布式压测用 JMeter、HTTP 全链路压测用 JMeter 或 Gatling、复杂场景编排用 JMeter。ContiPerf 做不到分布式压测也不支持 HTTP 协议以外的场景。但它胜在简单——一个注解定义参数一个注解校验阈值IDE 里跑一下就有报告。对 SpringBoot 项目来说想在开发阶段快速拿到接口性能数据ContiPerf 是投入产出比最高的选择。