
1. 项目概述当接口测试需要“自备干粮”做接口自动化测试尤其是性能压测前的数据准备最头疼的往往不是写脚本而是“造数据”。你可能会遇到这样的场景一个用户注册接口需要成千上万个不重复的手机号、邮箱和用户名一个下单接口需要关联有效的商品ID和用户Token。手动造效率太低还容易出错。用固定的几组数据循环跑在需要唯一性校验或者数据关联的业务里这招根本行不通。这次要聊的就是我在一次大规模接口自动化测试中为了解决“自动化造数”这个核心痛点折腾出来的一个方案用Jmeter的BeanShell脚本动态生成测试数据并实时写入Excel文件。听起来好像就是把数据存一下但实际做起来你会发现这不仅仅是“存一下”那么简单。它涉及到Jmeter脚本的逻辑编排、BeanShell与Java的交互、Excel文件的操作以及如何让整个流程在无头Headless的压测环境中稳定运行。这个方案的直接价值是它为后续的测试用例提供了“活”的数据源。比如我可以在一个“数据构造”线程组里批量生成用户数据并写入Excel然后在另一个“业务测试”线程组里通过CSV Data Set Config组件读取这个Excel去执行登录、下单等操作。数据从生成到使用形成了一个闭环真正实现了测试数据的自动化管理和复用。2. 核心思路与工具选型背后的考量为什么选择Jmeter BeanShell Excel这个组合而不是用Python写个脚本或者用Jmeter的插件这里面的取舍是基于实际项目环境和团队协作效率的综合判断。2.1 为什么是Jmeter首先我们的主体测试框架就是Jmeter。项目已经用Jmeter搭建了完整的接口测试和性能测试套件引入额外的Python或Java脚本框架会增加学习成本、环境依赖和集成复杂度。在Jmeter生态内解决问题是维护技术栈统一性和脚本可维护性的第一原则。所有测试工程师都熟悉Jmeter界面和元件调试和排查问题都在同一个平台沟通成本最低。2.2 为什么用BeanShell而不是JSR223Jmeter支持多种脚本语言如BeanShell、JSR223支持Groovy、JavaScript等。BeanShell是一个轻量级的Java脚本解释器语法几乎与Java一致。我选择它主要基于两点历史兼容性与稳定性在较早的Jmeter版本中BeanShell是内建且最成熟的脚本支持。虽然JSR223Groovy在性能上后来居上但在处理简单的Java IO操作如写文件时BeanShell的稳定性和直接性更让我放心尤其是在长时间运行的压测中避免因脚本引擎问题导致的内存泄漏或崩溃。直接操作Java APIBeanShell脚本可以直接调用Java SE的API。我们接下来要用的Apache POI库用于操作Excel就是一个Java库。在BeanShell中调用POI比在JSR223中处理语言间的桥接更为直接和清晰调试时堆栈信息也更友好。注意如果你的Jmeter版本较新如5.0以上且对脚本执行性能有极高要求JSR223Groovy确实是官方推荐的选择。但就“写Excel”这个具体任务而言BeanShell的简单可靠仍是巨大优势。2.3 为什么是Excel而不是CSV或数据库数据存储的选择有很多CSV文件、JSON文件、甚至内存数据库如H2。CSV虽然轻量但格式简单不支持多Sheet、单元格格式且容易被Excel软件误修改如长数字串被科学计数法显示。数据库功能强大但引入外部依赖增加了环境配置的复杂性在简单的数据传递场景中显得“杀鸡用牛刀”。Excel是一个折中且实用的选择。首先它被广泛接受测试和开发人员都能直接打开查看、验证数据。其次通过Apache POI库我们可以精确控制写入的格式如文本、数字避免数据被误解。最后Jmeter自带的“CSV Data Set Config”元件实际上可以读取.xlsx文件需将文件另存为CSV格式或使用插件但直接写入Excel为我们提供了更灵活的数据组织能力比如按业务模块分Sheet存放。核心思路总结在Jmeter测试框架内利用BeanShell脚本调用Java的Apache POI库将脚本运行时动态生成的数据如变量、接口响应结果以特定格式写入Excel文件为后续测试阶段提供结构化的数据源。整个方案追求的是内聚性不引入外部系统、可维护性代码在Jmeter内和实用性产出物易于人工查验。3. 环境准备与核心库导入工欲善其事必先利其器。在开始写脚本之前我们需要确保Jmeter环境具备了运行BeanShell和操作Excel的能力。3.1 Jmeter与JDK基础确保你安装的Jmeter版本是稳定版如5.4.1, 5.6.2。BeanShell功能是内置的无需额外安装。更重要的是JDK版本。因为BeanShell和POI库都运行在JVM上建议使用JDK 8或JDK 11这些长期支持版本。你可以在命令行输入java -version来确认。高版本JDK如17通常也兼容但为避免冷僻问题建议使用主流版本。3.2 引入关键武器Apache POI库Jmeter本身不能直接操作Excel我们需要借助第三方Java库——Apache POI。你需要手动将POI的JAR包添加到Jmeter的类路径中。下载POI库访问Apache POI官网下载最新稳定版的二进制发行包通常是poi-bin-*.zip。对于基本的Excel写入操作.xlsx格式我们主要需要以下核心JAR文件poi-*.jarpoi-ooxml-*.jarpoi-ooxml-schemas-*.jarxmlbeans-*.jarcommons-collections4-*.jarcommons-compress-*.jarcurvesapi-*.jar版本号*会随时间变化请确保这些JAR的版本相互兼容最简单的方法是使用官方发行包里提供的完整集合。放置JAR包将上述下载的所有JAR文件复制到Jmeter安装目录下的lib/ext文件夹中。lib/ext目录是Jmeter用于加载扩展库的标准位置放在这里的JAR会在Jmeter启动时被自动加载到类路径中。验证导入重启Jmeter。你可以通过添加一个“BeanShell Sampler”在脚本区域输入import org.apache.poi.ss.usermodel.*;并运行。如果没有报错说明POI库导入成功。如果出现ClassNotFoundException请检查JAR文件是否完整、是否放在了正确的目录并确保重启了Jmeter。实操心得管理Jmeter的依赖JAR是个细活。建议为这个项目单独建立一个文件夹存放所有需要的第三方JAR。这样当需要迁移或共享测试计划时你可以清晰地知道依赖项有哪些。另外避免将不同版本的相同JAR包混放在lib和lib/ext下这可能导致难以排查的类冲突。4. BeanShell脚本编写详解从变量到Excel单元格这是整个实例的核心部分。我们将创建一个BeanShell Sampler在其中编写完整的Excel写入逻辑。我会将脚本分块讲解并解释每一部分的作用。4.1 脚本骨架与变量定义首先我们规划一下要写入Excel的数据。假设我们要为性能测试构造用户数据每条数据包括用户ID自增、用户名、手机号、邮箱。手机号和邮箱需要保证唯一性。// BeanShell Sampler 脚本内容 import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.*; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; // 1. 定义文件路径和名称 String excelFilePath D:/TestData/Generated_Users.xlsx; // 请根据实际情况修改路径 File excelFile new File(excelFilePath); // 2. 定义要写入的数据这里模拟生成10条 int recordCount 10; String[][] userData new String[recordCount][4]; // 4列ID, UserName, Phone, Email // 生成随机数据填充数组 Random rand new Random(); SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmssSSS); String timestamp sdf.format(new Date()); String basePhone 13800138; // 手机号前缀 for (int i 0; i recordCount; i) { // 用户ID 从1开始自增 userData[i][0] String.valueOf(i 1); // 用户名 userData[i][1] TestUser_ timestamp.substring(8) _ (i1); // 加入时间戳和序号确保唯一 // 手机号 确保唯一且符合格式 int suffix 100 i; // 生成三位后缀如100,101... userData[i][2] basePhone String.format(%03d, suffix); // 邮箱 userData[i][3] user (i1) _ timestamp test.com; } // 3. 调用写入Excel的函数 writeDataToExcel(excelFile, userData); // 标记成功 vars.put(EXCEL_WRITE_STATUS, SUCCESS); SampleResult.setResponseMessage(成功生成 recordCount 条用户数据至: excelFilePath);代码解读导入包org.apache.poi.ss.usermodel.*包含了操作Excel所需的核心接口如Workbook, Sheet, Row, Cell。XSSFWorkbook是处理.xlsx格式的具体实现类。文件路径excelFilePath定义了Excel文件的生成位置。强烈建议使用绝对路径因为Jmeter可能从不同的工作目录启动相对路径容易导致文件找不到。数据构造我们用一个二维数组userData来模拟要写入的数据。在实际项目中这些数据可能来源于前一个接口的响应提取使用JSON Extractor或正则表达式提取器。使用BeanShell自带的Random、UUID等函数生成。从外部文件或数据库读取。唯一性保障在生成用户名、手机号、邮箱时我嵌入了时间戳和循环索引这是保证数据唯一性的常用技巧避免在重复执行脚本时产生冲突。状态传递vars.put(“EXCEL_WRITE_STATUS”, “SUCCESS”)将成功状态存入Jmeter变量可供后续逻辑判断使用。SampleResult.setResponseMessage用于在“查看结果树”中显示友好信息。4.2 核心写入函数writeDataToExcel实现接下来我们实现最关键的写入函数。这个函数负责处理Excel工作簿、工作表、行、列的创建和数据的填充。// 写入Excel的函数定义 void writeDataToExcel(File file, String[][] data) { FileOutputStream fos null; Workbook workbook null; try { // 判断文件是否存在 boolean fileExists file.exists(); if (fileExists) { // 文件存在则加载现有工作簿进行追加 FileInputStream fis new FileInputStream(file); workbook new XSSFWorkbook(fis); fis.close(); } else { // 文件不存在创建新的工作簿 workbook new XSSFWorkbook(); // 同时确保文件所在目录存在 file.getParentFile().mkdirs(); } // 获取或创建第一个工作表Sheet命名为“用户数据” Sheet sheet workbook.getSheet(用户数据); if (sheet null) { sheet workbook.createSheet(用户数据); // 如果是新建的Sheet可以创建表头 Row headerRow sheet.createRow(0); String[] headers {用户ID, 用户名, 手机号, 邮箱}; for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); // 可以简单设置一下表头样式 CellStyle headerStyle workbook.createCellStyle(); Font headerFont workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); cell.setCellStyle(headerStyle); } } // 确定写入的起始行追加到现有数据的末尾 int startRowNum sheet.getLastRowNum() 1; // getLastRowNum返回最后一行索引从0开始 // 循环写入数据行 for (int i 0; i data.length; i) { Row row sheet.createRow(startRowNum i); for (int j 0; j data[i].length; j) { Cell cell row.createCell(j); cell.setCellValue(data[i][j]); // 一个重要的技巧将手机号、ID等数字长串设置为文本格式防止Excel科学计数法显示 if (j 0 || j 2) { // 假设第0列是ID第2列是手机号 CellStyle textStyle workbook.createCellStyle(); DataFormat format workbook.createDataFormat(); textStyle.setDataFormat(format.getFormat()); cell.setCellStyle(textStyle); } } } // 自动调整列宽根据内容 for (int i 0; i data[0].length; i) { sheet.autoSizeColumn(i); } // 写入文件 fos new FileOutputStream(file); workbook.write(fos); log.info(数据已写入Excel文件: file.getAbsolutePath()); } catch (Exception e) { log.error(写入Excel文件时发生错误, e); // 将错误信息记录到Jmeter变量方便后续断言 vars.put(EXCEL_WRITE_ERROR, e.toString()); SampleResult.setSuccessful(false); // 标记采样器为失败 SampleResult.setResponseMessage(写入Excel失败: e.getMessage()); throw new RuntimeException(e); // 抛出异常停止当前线程 } finally { // 确保资源被关闭防止内存泄漏和文件锁 try { if (fos ! null) { fos.close(); } } catch (IOException e) { log.warn(关闭FileOutputStream时出错, e); } try { if (workbook ! null) { workbook.close(); } } catch (IOException e) { log.warn(关闭Workbook时出错, e); } } }代码解读与关键技巧追加模式与创建模式这是脚本的亮点之一。通过判断文件是否存在file.exists()我们实现了数据的追加写入。如果文件已存在可能是上次运行生成的就加载它并在末尾添加新数据如果不存在则创建新文件并添加表头。这非常适用于需要分批次生成大量测试数据的场景。表头处理只有在创建新Sheet时才生成表头。这避免了在追加数据时重复创建表头。格式处理——防止科学计数法这是处理Excel数据的一个经典坑。像手机号13800138100、长的数字ID如果直接以数字形式写入Excel会默认用科学计数法显示如1.38001E10导致数据失真。解决方案是将单元格格式显式设置为文本。我们通过cell.setCellStyle(textStyle)来实现。自动调整列宽sheet.autoSizeColumn(i)可以让Excel根据单元格内容自动调整列宽让生成的文件更美观易读。但注意对于大量数据或频繁操作这会有一定的性能开销在生产压测脚本中可酌情关闭。异常处理与资源管理在try-catch-finally块中我们捕获了所有异常并通过log.error输出到Jmeter日志同时将错误信息存入变量EXCEL_WRITE_ERROR。最关键的是在finally块中关闭FileOutputStream和Workbook对象。如果不关闭文件句柄可能一直被占用导致后续无法读写甚至引起内存泄漏。这是保证脚本健壮性的必备操作。日志输出使用log.info或log.error代替System.out.println日志会输出到Jmeter的日志面板或文件便于调试。5. 集成到Jmeter测试计划线程组与逻辑控制器单独的BeanShell Sampler只是一个“数据生成器”。要让它融入自动化测试流程需要合理的Jmeter元件进行组织和调度。5.1 测试计划结构设计一个典型的数据准备与消费测试计划结构如下测试计划 ├─ 用户自定义变量设置文件路径、生成数量等 ├─ 线程组数据构造组 (Thread Group: Data Builder) │ ├─ 循环控制器控制生成批次 │ │ └─ BeanShell Sampler (执行上述写入Excel的脚本) │ └─ 查看结果树 (用于调试正式运行时可禁用) └─ 线程组业务测试组 (Thread Group: Business Test) ├─ CSV Data Set Config (读取上面生成的Excel文件) ├─ HTTP请求采样器 (使用CSV中的数据作为参数) ├─ 响应断言等 └─ 聚合报告等监听器数据构造组这个线程组可能只运行1个线程循环N次比如循环10次每次生成100条数据。它负责“造数”并写入Excel。务必将其设置为在“业务测试组”之前运行。可以在测试计划中勾选“独立运行每个线程组”来手动控制顺序或者使用“定时器”和“模块控制器”进行更复杂的调度但通常顺序执行最简单可靠。业务测试组这个线程组使用CSV Data Set Config元件来读取Excel文件。需要注意的是CSV Data Set Config默认支持CSV格式。对于.xlsx文件你有两种选择推荐在BeanShell脚本中直接将数据写入为.csv格式。CSV文件更轻量读写更快且Jmeter原生支持。只需将文件后缀改为.csv并用FileWriter和BufferedWriter按逗号分隔写入即可无需POI库。继续使用.xlsx但需要将Excel文件另存为“CSV逗号分隔”格式或者使用第三方插件如“Jmeter Plugins Manager”中的“Custom JMeter Functions”可能提供相关函数。为了简化本例后续会补充一个写入CSV的BeanShell版本。5.2 参数化与动态路径在“用户自定义变量”中我们可以定义一些全局参数让脚本更灵活EXCEL_FILE_PATH:D:/TestData/Generated_Users.csvRECORD_COUNT_PER_LOOP:100DATA_PREFIX:PERF_TEST在BeanShell脚本中就可以通过vars.get(“EXCEL_FILE_PATH”)来获取这些变量值而不是将路径写死在代码里。这使得脚本更容易在不同环境开发、测试、生产间迁移。5.3 写入CSV格式的BeanShell脚本示例如果你决定采用CSV格式供Jmeter直接读取这里提供一个更轻量的BeanShell Sampler脚本import java.io.*; String csvFilePath vars.get(EXCEL_FILE_PATH); // 从变量获取路径例如 D:/TestData/users.csv int recordCount Integer.parseInt(vars.get(RECORD_COUNT_PER_LOOP)); String prefix vars.get(DATA_PREFIX); File csvFile new File(csvFilePath); boolean fileExists csvFile.exists(); BufferedWriter bw null; try { // 如果文件不存在先创建目录和文件并写入表头 if (!fileExists) { csvFile.getParentFile().mkdirs(); bw new BufferedWriter(new FileWriter(csvFile, true)); // true表示追加模式 // 写入CSV表头 bw.write(userId,userName,phone,email); bw.newLine(); } else { bw new BufferedWriter(new FileWriter(csvFile, true)); } // 生成并写入数据行 SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timestamp sdf.format(new Date()); Random rand new Random(); for (int i 0; i recordCount; i) { String userId prefix _ timestamp _ (i1); // 生成随机手机号简单模拟 String phoneSuffix String.format(%04d, rand.nextInt(10000)); String phone 1380013 phoneSuffix; String email user_ userId test.com; // 构造CSV行字段用逗号分隔。如果字段内可能包含逗号需要用双引号包裹。 String line String.format(%s,%s,%s,%s, userId, User_ userId, phone, email); bw.write(line); bw.newLine(); } bw.flush(); // 刷新缓冲区 log.info(成功追加 recordCount 条数据到CSV文件: csvFilePath); vars.put(CSV_WRITE_STATUS, SUCCESS); } catch (Exception e) { log.error(写入CSV文件失败, e); vars.put(CSV_WRITE_ERROR, e.toString()); SampleResult.setSuccessful(false); SampleResult.setResponseMessage(CSV写入失败: e.getMessage()); } finally { try { if (bw ! null) { bw.close(); } } catch (IOException e) { log.warn(关闭BufferedWriter时出错, e); } }这个脚本更简洁不依赖外部JAR生成的CSV文件可以直接被CSV Data Set Config使用。CSV Data Set Config的配置中将文件名指向这个CSV文件变量名称设置为userId,userName,phone,email分隔符用逗号即可。6. 常见问题、调试技巧与性能优化在实际使用中你肯定会遇到各种问题。下面是我踩过的一些坑和总结的应对方法。6.1 文件锁与并发写入冲突问题描述在多线程并发执行BeanShell Sampler写入同一个文件时可能会遇到“文件被占用”或“数据错乱”的异常。根因分析FileOutputStream或Workbook.write不是线程安全的。多个线程同时打开和写入同一个文件会导致冲突。解决方案单线程生成将数据生成线程组设置为仅1个线程。这是最简单有效的方法因为数据准备阶段通常不需要并发。文件分片如果必须并发生成大量数据可以让每个线程写入不同的文件。例如根据线程号${__threadNum}来生成不同的文件名users_${__threadNum}.csv。最后再用一个单独的步骤合并这些文件可以用BeanShell也可以用操作系统的命令。同步锁在BeanShell脚本中使用Java的同步机制但Jmeter的BeanShell引擎对多线程同步的支持并不直观且可能影响性能不推荐。6.2 内存溢出OOM问题问题描述当生成的数据量极大例如数十万行时可能会抛出OutOfMemoryError。根因分析Apache POI的XSSFWorkbook在处理大量数据时会将所有数据保存在内存中容易导致JVM堆内存耗尽。解决方案使用SXSSFWorkbook流式APIApache POI提供了SXSSFWorkbook它采用滑动窗口机制只将一部分行保留在内存中其余写入临时文件非常适合处理大数据量。将代码中的new XSSFWorkbook()替换为new SXSSFWorkbook(100)参数100表示在内存中保留的行数。import org.apache.poi.xssf.streaming.SXSSFWorkbook; ... Workbook workbook new SXSSFWorkbook(100); // 在内存中保留100行注意使用SXSSF后workbook.close()方法不仅会关闭工作簿还会删除临时文件这是必须调用的。分批写入在BeanShell脚本中控制每次写入的数据量比如每生成1000条数据就执行一次workbook.write(fos)并刷新流然后清空或重新创建Sheet这需要更复杂的逻辑。增大Jmeter堆内存在Jmeter启动脚本jmeter.bat或jmeter中调整HEAP参数例如set HEAP-Xms2g -Xmx4g。但这只是治标对于海量数据还是要靠方案1。6.3 数据格式错乱与乱码问题描述生成的Excel或CSV文件用文本编辑器打开正常但用Excel打开时中文显示乱码或单元格格式不对。解决方案中文乱码确保写入时指定正确的字符编码。对于CSV在创建FileWriter时指定UTF-8bw new BufferedWriter(new OutputStreamWriter(new FileOutputStream(csvFile, true), UTF-8));对于ExcelPOI其内部处理编码通常无需额外设置。如果仍有问题检查系统默认编码。数字格式问题如前所述长数字串务必设置为文本格式。对于CSV可以在字段前强制添加制表符\t或等号并包裹在双引号中提示Excel将其作为文本处理例如\13800138100\。但最通用的方法还是在Excel中手动设置列格式为“文本”。6.4 调试技巧充分利用日志在BeanShell脚本中多使用log.debug(“变量值: ” someVar)来输出中间变量。在Jmeter的“选项” - “日志查看器”中查看实时日志。使用调试采样器Debug Sampler在BeanShell Sampler前后添加Debug Sampler和View Results Tree可以查看Jmeter变量在脚本执行前后的变化非常有助于定位问题。简化与隔离当脚本出错时先注释掉文件操作部分只运行数据生成逻辑看变量是否正确。然后再单独测试文件写入功能使用固定的测试数据。检查文件权限确保Jmeter进程有权限在目标目录进行读写操作。特别是在Linux服务器上执行时目录权限和用户权限是常见问题。6.5 性能优化建议减少IO操作对于CSV写入使用BufferedWriter并一次性写入多行通过循环最后再flush()和close()比写一行刷一次效率高得多。谨慎使用自动列宽sheet.autoSizeColumn()在数据行很多时非常耗时。如果不需要完美的列宽显示可以移除这行代码。选择合适的格式如果Jmeter消费数据是主要场景优先使用CSV格式。它比Excel的.xlsx格式读写速度快几个数量级且不依赖外部JAR包。控制数据量根据测试实际需要生成数据避免生成过多无用数据。可以在“用户自定义变量”中通过参数灵活控制。7. 扩展应用更复杂的造数场景掌握了基础的数据写入后这个模式可以扩展到更多自动化测试场景关联数据构造测试订单流程需要先有用户再有商品最后生成订单。可以设计多个BeanShell Sampler第一个生成用户数据写入Sheet1第二个读取Sheet1的用户ID生成对应的商品数据写入Sheet2第三个再根据前两个Sheet的数据生成订单数据写入Sheet3。形成一个数据流水线。响应数据持久化将某个关键接口的响应结果如查询到的商品列表实时保存到Excel。这样不仅提供了测试数据还可以作为接口返回数据的快照用于后续的数据比对或审计。参数化测试报告将每次性能测试的关键指标如线程数、吞吐量、错误率连同时间戳一起写入Excel。长期积累下来这个Excel文件就成了一个简单的性能趋势分析报表。与配置元件结合使用User Defined Variables或Property来全局控制造数脚本的开关、路径和规模。例如设置一个属性auto.generate.datatrue在BeanShell脚本中通过props.get(“auto.generate.data”)读取从而实现在非造数阶段跳过整个数据构造线程组。这个由“自动化造数”需求引出的BeanShell写入Excel/CSV的实例其核心价值在于将Jmeter从一个单纯的“请求发送与接收器”升级为一个具备“数据生产与加工能力”的自动化测试平台。它解决了测试数据依赖的痛点让接口测试和性能测试的准备工作也变得自动化和可重复。虽然过程中需要处理一些文件IO、并发和格式的细节但一旦搭建成功它带来的效率提升和可靠性保障是非常显著的。下次当你面对一堆需要预先生成的测试数据时不妨试试这个思路让Jmeter帮你把“干粮”自动备好。