【jmeter教程——从入门到全精通】 体系化教程

第一章. 性能测试通识与环境搭建

摘要:本章作为全系列的基石,我们将首先厘清性能测试的核心指标(TPS、RT),接着快速搭建一个基于 Spring Boot 的待测应用,最后完成 JMeter 的安装与基础环境配置,为后续的压测实战做好准备。

本章学习路径

我们将按照以下步骤构建性能测试的实验环境:

  • 1.1 核心概念扫盲
    • 1.1.1 性能测试的本质与目的
    • 1.1.2 关键指标详解:$TPS$ 与 $RT$
  • 1.2 搭建待测靶场(Spring Boot)
    • 1.2.1 使用 IDEA 快速初始化项目
    • 1.2.2 编写模拟业务延迟的测试接口
    • 1.2.3 验证服务可用性
  • 1.3 JMeter 环境部署
    • 1.3.1 JDK 环境检查(前置条件)
    • 1.3.2 JMeter 下载与目录结构解析
    • 1.3.3 关键配置修改(中文支持/编码格式)

1.1. 核心概念扫盲:什么是 “抗揍” 的代码

在正式动手之前,我们必须先统一语言。很多开发者认为 “程序没报错” 就是开发完成了,但在高并发场景下,功能正常的代码可能会因为资源耗尽而导致服务雪崩。

1.1.1. 性能测试的定义

性能测试(Performance Testing)是通过自动化的工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。

对于 Spring Boot 开发者而言,它的核心价值在于:

  1. 识别瓶颈:找到系统的短板(是 CPU 算不过来,还是数据库连接不够用)。
  2. 规划容量:明确系统最大能支撑多少用户同时访问。
  3. 验证稳定性:确保系统在长时间高负载下不会内存溢出(OOM)。

1.1.2. 黄金指标:$TPS$ 与 $RT$

在后续的 JMeter 报告中,我们最关注两个指标,它们存在着一种制衡关系:

  1. 响应时间 ($RT$ - Response Time)
  • 定义:从客户端发起请求到接收到完整响应所消耗的时间。
    * 体感:用户觉得 “卡不卡”。
    * 标准:通常互联网应用要求核心接口 $RT < 200ms$。
  1. 每秒事务数 ($TPS$ - Transactions Per Second)
  • 定义:系统每秒钟能够处理的业务请求数量。
    * 体感:系统能 “抗多少人”。
    * 关系:在理想情况下,$TPS \approx \frac{并发数}{RT}$。

注意:$TPS$ 和 $RT$ 往往是反比关系。当并发过高导致系统拥堵时,$RT$ 会急剧上升,从而导致 $TPS$ 下降。寻找这两者的 “平衡点” 就是性能调优的目标。


1.2. 搭建待测靶场:Spring Boot 测试项目

在上一节中,我们明确了性能测试的目标。为了让后续的 JMeter 学习有真实的 “攻击目标”,我们需要先构建一个标准的 Spring Boot 应用。为了模拟真实的业务场景,我们不能只写一个简单的 Hello World,而需要模拟出一定的 “耗时”。

1.2.1. 项目初始化

我们将使用 IntelliJ IDEA 快速构建项目。

操作步骤

  1. 打开 IDEA,点击 New Project
  2. 选择 Spring Initializr
  3. Name: jmeter-demo
  4. JDK: 推荐选择 21(2025 年主流 LTS 版本),我们在后续会测试虚拟线程
  5. Java: 选择对应版本。
  6. Packaging: Jar。
  7. 点击 Next,在依赖选择中勾选:
    • Web -> Spring Web (提供 REST 接口支持)。
    • Lombok (简化代码,可选)。

image-20251119092046741

1.2.2. 编写模拟业务接口

我们需要编写一个 Controller,并模拟一定的业务处理时间(例如查询数据库耗时)。

文件路径

1
2
3
4
src/main/java/com/demo/jmeterdemo/
├── JmeterDemoApplication.java
└── controller/
└── TestController.java # 我们将创建这个文件

代码实现

我们首先定义一个简单的 REST 接口,使用 Thread.sleep 来模拟业务逻辑的耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.demo.jmeterdemo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

// 1. 定义 RestController
@RestController
@RequestMapping("/api/test")
public class TestController {

/**
* 模拟一个简单的业务查询接口
* 场景:用户查询商品详情,预计耗时 50ms - 100ms
*/
@GetMapping("/hello")
public Map<String, Object> hello() throws InterruptedException {
// 2. 模拟业务处理时间(随机延迟)
// 在真实压测中,接口一定是有耗时的,0ms 的接口没有压测意义
int sleepTime = ThreadLocalRandom.current().nextInt(50, 100);
Thread.sleep(sleepTime);

// 3. 封装返回结果
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "success");
result.put("data", "Processing time: " + sleepTime + "ms");
result.put("timestamp", System.currentTimeMillis());

return result;
}
}

代码解析

  • ThreadLocalRandom:在高并发环境下,它的性能优于 Random 类,用于生成随机数。
  • Thread.sleep:我们强制让线程休眠 50-100 毫秒。这是为了模拟真实项目中数据库查询或远程调用所需的 IO 等待时间。如果接口响应太快(0ms),JMeter 可能瞬间把 CPU 打满,无法观察到真实的并发排队现象。

1.2.3. 启动与验证

  1. 运行 JmeterDemoApplication.java 中的 main 方法。
  2. 打开浏览器访问:http://localhost:8080/api/test/hello
  3. 如果看到类似 {"code":200,"data":"Processing time: 76ms" ...} 的 JSON 返回,说明靶场搭建成功。

1.3. JMeter 环境部署

靶场已经就位,现在我们需要部署压测工具 JMeter。JMeter 是基于 Java 开发的,因此必须依赖 JDK 环境。

1.3.1. 前置检查:JDK

打开终端(Command Prompt 或 Terminal),输入以下命令:

1
java -version
  • 成功:输出 java version "17.0.x" 或更高版本。
  • 失败:如果提示 'java' is not recognized(‘java’ 不是内部或外部命令),请先安装 JDK 并配置 JAVA_HOME 环境变量。这是 Java 开发的基本功,不再赘述。

1.3.2. 下载与安装

Apache JMeter 是免安装的绿色软件。

  1. 下载:访问 Apache JMeter 官网
  2. 选择版本:找到 Binaries(二进制版)区域。
    • Windows 用户下载 .zip 文件。
    • Mac/Linux 用户下载 .tgz 文件。
    • 注意:不要下载 Source(源码版),那是给开发者看源码用的。
  3. 解压:将压缩包解压到一个 没有中文、没有空格 的路径下(例如 D:\tools\apache-jmeter-5.6.3/usr/local/jmeter)。

目录结构说明

  • bin/:存放启动脚本(jmeter.bat, jmeter.sh)和配置文件(jmeter.properties)。
  • lib/:存放 JMeter 的核心 Jar 包和第三方插件。
  • docs/:官方文档。

1.3.3. 基础配置优化

默认的 JMeter 配置是英文界面且编码可能存在问题,我们需要在启动前做一次 “手术”。

文件路径[JMeter安装目录]/bin/jmeter.properties

使用文本编辑器(如 Notepad++ 或 VS Code)打开该文件,完成以下修改:

image-20251119092512755

1. 界面语言设置为中文
JMeter 原生支持中文,修改配置可永久生效。

1
2
# 搜索 language,去掉前面的 # 号
language=zh_CN

2. 强制使用 UTF-8 编码
这是为了防止压测过程中响应数据出现中文乱码。

1
2
# 搜索 sampleresult.default.encoding
sampleresult.default.encoding=UTF-8

3. 启动验证

  • Windows:双击 bin/jmeter.bat
  • Mac/Linux:在终端执行 sh bin/jmeter.sh

常见错误:启动时如果出现终端一闪而过,通常是因为 JAVA_HOME 环境变量未配置正确。

如果成功看到了 JMeter 的图形化界面(GUI),并且菜单栏是中文的,恭喜你,所有的准备工作已经就绪。


1.4. 本章小结

在本章中,我们完成了从理论认知到环境搭建的全过程。现在,你的电脑上同时运行着一个 “脆弱” 的 Spring Boot 应用和一个 “强大” 的压测工具。

核心要点

  1. TPS 与 RT:$TPS$ 是吞吐量,$RT$ 是延迟,两者通常呈反比。
  2. 测试靶场:压测必须基于具有真实业务耗时(Sleep)的接口,0ms 的接口没有测试价值。
  3. 环境隔离:JMeter 安装路径严禁包含中文或空格,否则会导致未知的 Java 类加载错误。

下一步计划:我们的 “枪”(JMeter)和 “靶子”(Spring Boot)都准备好了。在下一章,我们将编写第一个 JMeter 测试脚本,真正地扣动扳机,看看我们的 Spring Boot 应用在 100 个并发线程下表现如何。


第二章. 第一次亲密接触:压测你的 Spring Boot 接口

摘要:本章我们将正式启动 JMeter,编写第一个测试脚本。我们将深入理解 JMeter 的 “核心三剑客”(线程组、取样器、监听器),并对上一章编写的 Spring Boot 接口发起真实的并发调用,最后通过聚合报告学会看懂基本的性能数据。

本章学习路径

我们将通过以下步骤完成第一次压测实战:

  • 2.1 JMeter 核心逻辑架构
    • 2.1.1 核心三剑客:测试计划的骨架
    • 2.1.2 线程组与并发用户的映射关系
  • 2.2 编写第一个压测脚本
    • 2.2.1 配置线程组(模拟用户)
    • 2.2.2 配置 HTTP 请求(发起攻击)
    • 2.2.3 添加结果树(查看明细)
  • 2.3 运行与数据解读
    • 2.3.1 成功与失败的标志
    • 2.3.2 聚合报告核心指标解读
    • 2.3.3 GUI 模式的性能陷阱

2.1. JMeter 核心逻辑架构

在上一章中,我们已经准备好了基于 com.demo.jmeterdemo 包结构的 Spring Boot 项目。现在打开 JMeter,面对空空如也的界面,我们需要先理解它的构建逻辑。

JMeter 的脚本编写其实就是在 “搭积木”,而最核心的三块积木被称为 “三剑客”

2.1.1. 核心三剑客

组件名称英文名称对应现实场景作用
线程组Thread Group用户决定有多少人来访问,以及这些人进来的速度(并发数)。
取样器Sampler动作决定用户做什么操作(打开网页、调用接口、查询数据库)。
监听器Listener报表负责收集测试结果,展示为图表、表格或日志。

2.1.2. 线程组 = 虚拟用户

在 Java 开发中,我们知道 “线程(Thread)” 是 CPU 调度的基本单位。在 JMeter 中,一个线程就严格对应一个虚拟用户

  • 如果设置 10 个线程,就意味着有 10 个用户同时在这个时间点操作。
  • 这与 Spring Boot 中的 Tomcat 线程池是对应的:客户端发起 10 个线程请求,服务端就需要分配资源来处理这 10 个请求。

2.2. 编写第一个压测脚本

理论清楚后,我们开始动手。请确保你的 Spring Boot 项目(JmeterDemoApplication)已启动,且端口为 8080

2.2.1. 第一步:创建线程组(模拟用户)

操作步骤

  1. 右键点击 “测试计划 (Test Plan)” -> 添加 (Add) -> 线程 (Threads) -> 线程组 (Thread Group)

image-20251119092909690

  1. 在右侧面板配置以下参数:
参数名建议值含义解释
名称模拟前台用户给脚本起个可读的名字,便于管理。
线程数 (Number of Threads)10模拟 10 个用户并发访问。
Ramp-Up 时间 (Ramp-Up Period)1让这 10 个用户在 1 秒内陆续启动(即每 0.1 秒启动一个),避免瞬间压力过大导致系统误判。
循环次数 (Loop Count)10每个用户执行 10 次操作。总请求数 = $10 \times 10 = 100$。

关于 Ramp-Up 的思考:如果设置线程数为 100,Ramp-Up 为 0,意味着 100 人在同一毫秒瞬间涌入(秒杀场景)。如果设置线程数为 100,Ramp-Up 为 10,意味着每秒进来 10 人(常规业务场景)。

2.2.2. 第二步:配置 HTTP 请求(定义动作)

我们要调用的接口地址是:GET http://localhost:8080/api/test/hello

操作步骤

  1. 右键点击刚才创建的 “线程组” -> 添加 -> 取样器 (Sampler) -> HTTP 请求 (HTTP Request)

image-20251119093442038

  1. 填写核心配置:
    • 协议 (Protocol)http
    • 服务器名称或 IPlocalhost
    • 端口号8080
    • 方法 (Method)GET
    • 路径 (Path)/api/test/hello (注意不要包含 host 和 port,且以 / 开头)

2.2.3. 第三步:添加监听器(查看结果)

为了验证脚本是否配置正确,我们需要一个能够看到详细请求响应的组件。

操作步骤

  1. 右键点击 “线程组” -> 添加 -> 监听器 (Listener) -> 察看结果树 (View Results Tree),这样就够了

2.3. 运行与数据解读

脚本编写完成,点击工具栏上的绿色 启动按钮 (Start)。记得先保存脚本,通常命名为 hello-test.jmx

2.3.1. 察看结果树:调试神器

运行结束后,点击左侧的 “察看结果树”。你会在列表中看到 100 条记录(10 线程 x 10 循环)。

  • 绿色盾牌:表示请求成功(HTTP 状态码 200)。
  • 红色感叹号:表示请求失败。

点击任意一条成功的记录,切换到 响应数据 (Response Data) 选项卡:

1
2
3
4
5
6
{
"code": 200,
"message": "success",
"data": "Processing time: 85ms",
"timestamp": 1731980000123
}

image-20251119093724251

如果你看到了我们在 Spring Boot 代码中定义的 JSON 返回,说明 网络连通性接口逻辑 都没有问题。

2.3.2. 聚合报告:宏观视角

“察看结果树” 只能看单次请求的细节,无法评估整体性能。我们需要添加 “聚合报告” 来查看 TPS 和 RT。

操作步骤

  1. 右键点击 “线程组” -> 添加 -> 监听器 -> 聚合报告 (Aggregate Report)
  2. 清除数据:点击工具栏的小扫把(清除全部),然后重新运行一次测试。

核心指标解读

image-20251119093834565

指标 (Label)含义理想状态
样本 (Samples)总请求数应等于 线程数 x 循环次数。
平均值 (Average)平均响应时间 (ms)越低越好。但在长尾效应下,参考价值不如 99% 线。
99% 百分位 (99% Line)最重要的指标表示 99% 的请求都在这个时间内完成了。这代表了绝大多数用户的体验。
异常 % (Error %)错误率必须为 0.00%。任何报错都需要排查。
吞吐量 (Throughput)TPS每秒处理请求数。越高越好(前提是报错率为 0)。

实战分析:假设你的聚合报告显示:

  • Average: 75ms
  • Throughput: 120.5/sec

这说明在当前并发下,我们的 Spring Boot 应用每秒能处理约 120 个请求,平均每个请求处理耗时 75ms(这与我们在代码中写的 Thread.sleep(50~100) 吻合)。

2.3.3. 重要警告:GUI 模式的陷阱

新手必读:在使用 JMeter 进行 正式的大规模压测(例如成千上万并发)时,严禁 开启 “察看结果树” 和 “聚合报告” 等 GUI 监听器。

因为 GUI 组件渲染图表和记录实时日志会消耗大量的客户端 CPU 和内存资源。这会导致 JMeter 自身成为瓶颈,测量出的数据不准确(比如 TPS 上不去是因为你电脑卡了,而不是服务器卡了)。

最佳实践

  • GUI 模式:仅用于编写、调试脚本(验证通不通)。
  • CLI (命令行) 模式:用于正式执行压测(验证快不快)。我们将在后续章节详细讲解。

2.4. 本章小结

在本章中,我们打通了 JMeter 到 Spring Boot 的第一条测试链路。

核心要点

  1. 三剑客:线程组(人)、取样器(动作)、监听器(结果)是构建脚本的基石。
  2. Ramp-Up:用于控制用户进入系统的速率,避免瞬间洪峰导致测试失真。
  3. 指标分析:重点关注 99% Line(绝大多数人的体验)和 Throughput(系统处理能力),而不是简单的平均值。
  4. GUI 限制:调试用 GUI,压测用 CLI。

下一步计划:目前的脚本虽然能跑,但所有参数都是 “写死” 的。在真实业务中,每个用户的 ID、Token 或者查询的商品都不一样。在下一章,我们将学习 变量与参数化,让我们的压测脚本学会 “千人千面”。


第三章. 变量与参数化:告别硬编码

摘要:本章我们将解决测试脚本 “千人一面” 的问题。通过引入用户自定义变量、CSV 数据文件和随机函数,我们将把硬编码的死数据转化为动态的活数据,模拟真实世界中 “千人千面” 的并发访问场景。

本章学习路径

我们将按照以下步骤改造我们的测试脚本:

  • 3.1 升级靶场
    • 3.1.1 增加带参接口
    • 3.1.2 理解缓存对压测的干扰
  • 3.2 用户自定义变量
    • 3.2.1 提取全局环境配置(Host/Port)
    • 3.2.2 实现环境一键切换
  • 3.3 CSV 数据驱动
    • 3.3.1 准备测试数据文件
    • 3.3.2 配置 CSV Data Set Config
    • 3.3.3 循环读取机制详解
  • 3.4 随机化函数
    • 3.4.1 函数助手的使用
    • 3.4.2 UUID 与随机数字生成

3.1. 升级靶场:拒绝 “假” 压测

在上一节中,我们测试的是 /api/test/hello 接口。无论发多少次请求,参数和结果都是一样的。

但在真实的数据库压测中,如果 1000 个用户都查询同一个 id=1 的商品,数据库会利用 Buffer Pool(缓冲池) 机制将数据缓存到内存中。这意味着后续的 999 次请求根本没有触达磁盘 IO,这样的压测结果是失真的(看似 TPS 很高,实际上线就挂)。

为了模拟真实场景,我们需要一个能接收参数的接口。

3.1.1. 编写带参接口

文件路径src/main/java/com/demo/jmeterdemo/controller/TestController.java

请在原有的 Controller 中添加一个新的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 模拟商品详情查询接口
* 接收 URL 路径参数,模拟不同商品的查询
*/
@GetMapping("/product/{id}")
public Map<String, Object> getProduct(@PathVariable("id") String id) throws InterruptedException {
// 1. 模拟随机业务耗时 (20-50ms)
int sleepTime = ThreadLocalRandom.current().nextInt(20, 50);
Thread.sleep(sleepTime);

// 2. 封装返回结果
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
// 将传入的 ID 原样返回,便于我们在 JMeter 中验证参数是否生效
result.put("data", "Product ID: " + id);
result.put("cost", sleepTime + "ms");

return result;A
}

重启项目,访问 http://localhost:8080/api/test/product/999 进行验证。


3.2. 用户参数:环境一键切换

在开发过程中,我们经常需要在 本地环境 (Local)测试环境 (Dev)生产环境 (Prod) 之间切换。如果你的脚本里写死了 localhost,每次切换环境都要把几十个 HTTP 请求挨个修改一遍,这简直是噩梦。

3.2.1. 定义全局变量

操作步骤

  1. 点击 JMeter 左侧最顶层的 测试计划 (Test Plan)
  2. 右键点击 “测试计划” -> 添加 -> 前置处理器 -> 用户变量
  3. 点击 添加,输入以下键值对:
名称 (Name)值 (Value)描述
target_hostlocalhost目标服务器地址
target_port8080目标端口

image-20251119105551476

3.2.2. 引用变量

回到我们在第二章创建的 HTTP 请求

  1. 服务器名称或 IP 中的 localhost 替换为 ${target_host}
  2. 端口号 中的 8080 替换为 ${target_port}

image-20251119105609548

语法说明${variable_name} 是 JMeter 中引用变量的标准语法。

现在,如果你想切换到测试服务器,只需要在测试计划最顶层修改一次 target_host 的值,所有引用的地方都会自动生效。


3.3. CSV 数据驱动:模拟批量用户

现在我们要模拟 100 个不同的用户,查询 100 个不同的商品。我们将使用 CSV 文件来管理这些数据。

3.3.1. 准备数据文件

  1. 在电脑任意位置(建议在 JMeter 脚本同目录下)创建一个名为 data.csv 的文件。
  2. 输入几行测试数据(不需要表头,直接写内容):
1
2
3
4
5
1001
1002
1003
1004
1005

3.3.2. 配置 CSV Data Set Config

这是 JMeter 中最常用的元件之一。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 配置元件 (Config Element) -> CSV 数据文件设置 (CSV Data Set Config)
  2. 重要:将该组件拖拽到 “HTTP 请求” 的 上方(执行顺序很重要)。

image-20251119110127152

关键参数配置

参数项配置值解释
文件名 (Filename)data.csv如果文件在脚本同目录,直接写文件名;否则写绝对路径。
文件编码 (File encoding)UTF-8防止中文参数乱码。
变量名称 (Variable Names)p_id给 CSV 中的列起个变量名。如果有两列,用逗号隔开,如 username,password
遇到文件结束符再次循环?True如果文件只有 5 行,但线程循环 10 次,设为 True 会从头重新读取;设为 False 则会停止测试。
线程共享模式All threads所有线程共享这份文件。线程 1 取第一行,线程 2 取第二行,互不重复。

3.3.3. 实战调用

  1. 打开 HTTP 请求
  2. 修改 路径 (Path) 为:/api/test/product/${p_id}
  3. 启动测试。

查看 察看结果树,你会发现请求路径变成了:

  • /api/test/product/1001
  • /api/test/product/1002

这意味着 CSV 数据已成功注入。


3.4. 随机化函数:动态生成数据

有时候我们不需要固定的数据,而是需要唯一的随机数据(比如生成这就唯一的订单号,或者模拟随机的用户行为)。JMeter 提供了强大的 函数助手

3.4.1. 函数助手 (Function Helper)

这是 JMeter 的内置外挂。

操作步骤

  1. 点击 JMeter 顶部菜单栏的 工具 (Tools) -> 函数助手对话框 (Function Helper Dialog)
  2. 这是一个独立的弹窗,里面列出了所有可用函数。

3.4.2. 常用函数实战

我们修改脚本,在查询商品的同时,传递一个随机生成的 Trace-ID 请求头。

场景 1:生成随机数字

  1. 在函数助手中选择 __Random
  2. 最小值:1000,最大值:9999
  3. 点击 生成,你会得到字符串:${__Random(1000,9999,)}

场景 2:生成 UUID (全球唯一标识)

  1. 选择 __UUID
  2. 点击 生成,得到:${__UUID}

应用到 HTTP 请求:在 HTTP 请求面板,点击底部的 添加 按钮(参数区域):

  • 名称request_id
  • ${__UUID}

再次运行测试,查看结果树中的 请求数据 (Request Body/Header),你会看到类似 request_id=550e8400-e29b-41d4-a716-446655440000 的动态参数。

image-20251119111857411


3.5. 本章小结

在本章中,我们将静态的脚本进化为了动态脚本。

核心要点

  1. 变量引用:使用 ${var_name} 语法可以引用任何地方定义的变量。
  2. CSV 数据集:这是实现 “参数化” 的核心手段,特别适用于大量账号登录、批量查询等场景。线程共享模式 决定了数据是在线程间共享还是独享。
  3. 函数助手:善用 ${__Random}${__UUID} 可以模拟离散的流量,避免热点数据造成的缓存假象。

思考题:如果你在 CSV 中配置了 100 个用户账号,并且启动了 200 个线程,同时设置 “遇到文件结束符再次循环” 为 False,会发生什么?
(答案:前 100 个线程会成功获取数据,后 100 个线程会因为取不到数据而停止执行。)

下一步计划:现在的接口都是独立的。但在实际业务中,往往是 “用户登录” -> “获取 Token” -> “拿着 Token 下单”。这涉及到了接口之间的数据传递。下一章,我们将学习性能测试中最关键的技术——关联 (Correlation),教你如何从上一个接口的响应中提取数据传递给下一个接口。


第四章. 关联:搞定 Spring Security 认证

摘要:本章我们将攻克性能测试中最大的拦路虎——接口依赖。我们将模拟真实的 “登录 -> 获取 Token -> 携带 Token 访问受保护接口” 的业务闭环,掌握 JSON 提取器和正则表达式提取器的核心用法,实现跨请求的数据传递。

本章学习路径

我们将按照以下步骤构建动态的业务链路:

  • 4.1 场景与靶场升级
    • 4.1.1 什么是 “关联” (Correlation)
    • 4.1.2 模拟 Spring Security 登录与鉴权
  • 4.2 JSON 提取器实战
    • 4.2.1 JSONPath 语法速成
    • 4.2.2 提取 Token 变量
  • 4.3 跨请求传递
    • 4.3.1 HTTP 信息头管理器的使用
    • 4.3.2 调试与验证变量传递
  • 4.4 正则表达式提取器(进阶)
    • 4.4.1 万能的 Regex 语法
    • 4.4.2 提取响应头中的 Cookie

4.1. 场景与靶场升级:模拟真实的业务链

在上一章,我们通过随机参数模拟了独立的查询请求。但在现实世界中,90% 的业务操作都需要先 “登录”。

在 Spring Boot + Spring Security 的架构中,通常采用 JWT (JSON Web Token) 机制:

  1. 客户端调用 /login,服务端校验账号密码,返回一个加密字符串(Token)。
  2. 客户端在后续请求头中携带 Authorization: Bearer {Token}
  3. 服务端拦截器校验 Token 有效性。

为了在 JMeter 中实现这一过程,我们需要先升级我们的 Spring Boot 靶场。

4.1.1. 编写模拟鉴权接口

为了专注于 JMeter 本身,我们不引入笨重的 Spring Security 依赖,而是手动编写代码模拟这一行为。

文件路径src/main/java/com/demo/jmeterdemo/controller/AuthController.java

请创建新的控制器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.demo.jmeterdemo.controller;

import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

// 模拟一个简单的内存 Token 存储,用于验证
private static final String VALID_TOKEN_PREFIX = "Bearer ";

/**
* 1. 登录接口
* 作用:接收账号密码,返回 Token
*/
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> user) {
String username = user.get("username");
String password = user.get("password");

Map<String, Object> result = new HashMap<>();

// 模拟简单的账号校验
if ("admin".equals(username) && "123456".equals(password)) {
result.put("code", 200);
result.put("msg", "Login Success");
// 生成一个模拟的 Token (真实场景中是 JWT 字符串)
result.put("token", UUID.randomUUID().toString());
} else {
result.put("code", 401);
result.put("msg", "Invalid Credentials");
}
return result;
}

/**
* 2. 受保护的订单接口
* 作用:必须携带 Token 才能访问
*/
@GetMapping("/order/create")
public Map<String, Object> createOrder(@RequestHeader(value = "Authorization", required = false) String authHeader) {
Map<String, Object> result = new HashMap<>();

// 校验 Token 是否存在
if (authHeader == null || !authHeader.startsWith(VALID_TOKEN_PREFIX)) {
result.put("code", 403);
result.put("msg", "Forbidden: Missing or Invalid Token");
return result;
}

// 校验通过,执行业务
result.put("code", 200);
result.put("msg", "Order Created Successfully");
result.put("orderId", System.currentTimeMillis());
return result;
}
}

重启项目,我们有了两个新目标:

  1. POST /api/auth/login:获取 Token。
  2. GET /api/auth/order/create:需要 Token 才能访问。

4.2. JSON 提取器实战:抓取 Token

现在回到 JMeter。如果我们直接按顺序调用这两个接口,第二个接口必然报错(403 Forbidden),因为 JMeter 默认是一个 “健忘” 的客户端,它不会自动把上一次请求的响应带到下一次。

我们需要手动建立关联,这一步在 JMeter 中通过 后置处理器 (Post Processor) 实现。

4.2.1. 第一步:配置登录请求

  1. 创建一个新的 线程组,命名为 " S01_下单流程 "。

  2. 添加 HTTP 请求,命名为 " API_登录 "。

配置如下参数:

  • Method: POST
  • Path: /api/auth/login
  • 消息体数据:
1
2
3
4
{
"username": "admin",
"password": "123456"
}
  1. 重要:因为我们要发送 JSON Body,必须添加 HTTP 信息头管理器 (HTTP Header Manager)

配置步骤如下:

右键点击 " API_登录 " -> 添加 -> 配置元件 -> HTTP 信息头管理器。

添加一行:Content-Type = application/json

4.2.2. 第二步:添加 JSON 提取器

我们的目标是从登录接口返回的 JSON 中提取 token 字段的值。

1
2
3
4
5
{
"code": 200,
"msg": "Login Success",
"token": "550e8400-e29b-41d4-a716-446655440000" <-- 我们要抓取这个
}

操作步骤

  1. 右键点击 " API_登录 " -> 添加 -> 后置处理器 -> JSON 提取器 (JSON Extractor)

  2. 配置核心参数:

参数名配置值解释
变量名称 (Names of created variables)jwt_token给提取出来的值起个名字,后续用 ${jwt_token} 引用。
JSON 路径表达式 (JSON Path expressions)$.token$ 代表根节点,.token 代表下一级 key。
匹配号 (Match No.)1如果有多个 token,取第 1 个。0 代表随机,-1 代表全部。
默认值 (Default Value)TOKEN_NOT_FOUND如果提取失败,变量会被赋值为这个字符串(便于排查错误)。

4.3. 跨请求传递:使用 Token

拿到了 Token,下一步是将其 “注入” 到下单接口的请求头中。

4.3.1. 配置下单请求

  1. 在线程组下添加第二个 HTTP 请求,命名为 " API_创建订单 "。

配置如下参数:

  • Method: GET
  • Path: /api/auth/order/create
  1. 关键步骤:添加请求头。

操作步骤如下:

  • 右键点击 " API_创建订单 " -> 添加 -> 配置元件 -> HTTP 信息头管理器
  • 添加一行配置:
      • 名称*: Authorization
      • *: Bearer ${jwt_token}

注意:这里的值必须严格遵循后端代码的校验规则(即前缀 Bearer + 空格 + 变量)。

4.3.2. 调试与验证

点击启动运行测试,观察 察看结果树

  1. API_登录:响应数据中包含 Token。

  2. API_创建订单

验证步骤如下:

  • 点击 请求 (Request) 选项卡 -> Request Headers
  • 你应该能看到 Authorization: Bearer 550e8400...
  • 如果看到 Authorization: Bearer TOKEN_NOT_FOUND,说明 JSON 提取器配置有误(通常是 JSONPath 写错了)。
  • 如果响应状态码为 200msg: "Order Created Successfully",说明关联成功!

image-20251119114736843


4.4. 正则表达式提取器:处理非 JSON 场景

在现代前后端分离开发中,JSON 确实是主流。但在企业级开发中,我们经常会遇到 “老旧系统”(Legacy System)或第三方回调接口,它们返回的可能不是标准的 JSON,而是 XML、HTML 甚至纯文本字符串。此外,像 Set-CookieLocation 重定向地址等关键信息,往往隐藏在响应头(Response Headers)而非响应体中。

面对这些场景,JSON 提取器就失效了。这时,我们需要请出 JMeter 的万能军刀——正则表达式提取器

4.4.1. 场景升级:模拟 “老旧” 接口

为了演示这个场景,我们需要在 Spring Boot 靶场中增加一个模拟的老旧接口。它不返回 JSON,而是返回一段具有特定格式的字符串。

修改文件src/main/java/com/demo/jmeterdemo/controller/AuthController.java

请在 AuthController 类中添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 3. 模拟老旧系统的状态查询接口
* 返回格式:纯文本 "User: admin|SessionID: abc-123-xyz|Status: Active"
* 场景:我们需要提取 SessionID 用于后续操作
*/
@GetMapping("/legacy/status")
public String getLegacyStatus() {
// 模拟生成一个动态的 SessionID
String sessionId = "sess_" + System.currentTimeMillis();
// 返回非 JSON 格式的字符串
return String.format("User:admin|SessionID:%s|Status:Active", sessionId);
}

重启项目,访问 http://localhost:8080/api/auth/legacy/status,你会看到类似这样的响应:
User:admin|SessionID:sess_1731981234567|Status:Active

挑战目标:我们需要从这串文本中精准提取出 sess_1731981234567

4.4.2. 核心语法拆解

在使用提取器之前,我们必须先搞懂正则表达式在 JMeter 中的特殊语法。

我们要提取的目标是 SessionID:| 之间的内容。

正则表达式公式SessionID:(.*?)\|

  • SessionID:左边界。告诉 JMeter 从哪里开始找(锚点)。
  • ()捕获组。括号里的内容就是我们真正想要的数据。
  • .:匹配任意字符。
  • *:匹配 0 次或多次。
  • ?非贪婪模式(关键!)。
    • 如果不加 ?(贪婪模式),它会一直匹配到这一行的最后一个 |
    • 加上 ?,它会匹配到 最近的 一个 | 就停止。
  • \|右边界。因为 | 在正则中是特殊字符(表示 “或”),所以需要用 \ 转义。

4.4.3. 实战配置:提取 SessionID

现在我们来配置 JMeter 脚本。

操作步骤

  1. 添加请求:在线程组下添加一个新的 HTTP 请求,命名为 " API_老旧接口 "。
  • Path: /api/auth/legacy/status
    * Method: GET
  1. 添加提取器:右键点击 " API_老旧接口 " -> 添加 -> 后置处理器 -> 正则表达式提取器 (Regular Expression Extractor)

  2. 详细配置(请严格按照下表填写):

参数项配置值深度原理解析
要检查的响应字段主体 (Body)因为我们的数据在 Response Body 里。如果要提取 Cookie,这里需选 信息头
引用名称legacy_sess提取后的变量名,后续用 ${legacy_sess} 使用。
正则表达式SessionID:(.*?)|见上文的语法拆解。
模板 (Template)$1$$1$ 表示提取第 1 个括号对捕获的内容。如果是 $0$ 则表示提取整个表达式(包含边界)。
匹配数字 (Match No.)1如果响应中有多个 SessionID,填 1 取第一个;填 0 会随机取一个(常用于随机点击链接)。
缺省值ERR_SESS最佳实践:永远给一个显眼的错误默认值,方便后续 Debug。

4.4.4. 验证与调试

提取配置好了,但正则很容易写错(比如漏了转义符)。我们如何验证它是否工作正常?

方法一:使用 Debug Sampler(推荐)

  1. 在线程组中添加一个 调试取样器 (Debug Sampler)(位于:添加 -> 取样器 -> Debug Sampler)。
    • 这个组件的作用是把当前线程所有的变量都打印出来。
  2. 运行脚本。
  3. 察看结果树 中点击 Debug Sampler 的响应数据。
  4. 搜索 legacy_sess
    • 成功legacy_sess=sess_1731981234567
    • 失败legacy_sess=ERR_SESS(此时需要回头检查正则表达式)

方法二:正则测试器(RegExp Tester)

JMeter 的 “察看结果树” 自带了一个正则测试工具,不需要反复运行脚本 即可调试。

  1. 点击 察看结果树,选择 " API_老旧接口 " 的请求记录。
  2. 将结果树面板中的下拉框(默认是 Text)切换为 RegExp Tester
  3. Regular expression 输入框中填入 SessionID:(.*?)\|
  4. 点击 Test 按钮。
  5. 界面下方会直接显示提取结果。这是调试复杂正则最高效的方法。

4.5. 本章小结

在本章中,我们攻克了性能测试中 “数据流通” 的难题。

核心要点

  1. JSON 提取器:处理标准 REST API 的首选,简单高效,认准 $.key 语法。
  2. 正则提取器:处理非标数据(如老旧系统、HTML、Header 头信息)的终极方案。
    • 口诀左边界(想要的内容)右边界
    • 注意:慎用贪婪模式,善用 ? 限制匹配范围。
  3. 调试技巧:善用 Debug Sampler 查看变量池,或利用 RegExp Tester 实时验证正则逻辑。

下一步计划:现在我们的脚本已经能够成功登录并提取 Token 和 SessionID。但是,如果后端突然报错了(HTTP 500),或者返回了 {"code": 20001, "msg": "库存不足"},JMeter 默认还是会显示 “绿色盾牌”(因为 HTTP 状态码是 200)。这会导致我们误判测试结果。在下一章,我们将学习 断言 (Assertion),给 JMeter 装上 “火眼金睛”,让它能识别真正的业务成功。


第五章. 断言与逻辑控制:让脚本有 “脑子”

摘要:本章将解决 JMeter “盲目乐观” 的问题。默认情况下,只要服务器返回 HTTP 200,JMeter 就会判定测试通过,忽略了业务逻辑报错(如 “库存不足”)。我们将学习如何使用 断言 识别真正的业务失败,并利用 逻辑控制器 让脚本根据上一步的结果智能决定下一步的走向。

本章学习路径

我们将按照以下步骤打造智能脚本:

  • 5.1 揭穿 “假成功” 现象
    • 5.1.1 构造一个 “HTTP 200 但业务失败” 的接口
    • 5.1.2 观察 JMeter 的误判
  • 5.2 响应断言 (Response Assertion)
    • 5.2.1 校验核心业务字段
    • 5.2.2 设定自定义的失败消息
  • 5.3 JSON 断言 (JSON Assertion)
    • 5.3.1 精准校验结构化数据
    • 5.3.2 验证数组长度与特定值
  • 5.4 逻辑控制器 (If Controller)
    • 5.4.1 场景:登录失败就不下单
    • 5.4.2 JEXL3 表达式语法实战

5.1. 揭穿 “假成功” 现象

在上一章,我们完成了关联。但在实际开发中,接口返回 HTTP 200 并不代表业务成功。例如支付接口返回 {"code": 5001, "msg": "余额不足"},HTTP 状态码依然是 200。如果不加判断,JMeter 会认为这次请求是成功的,导致最终的压测报告显示 “100% 成功率”,这不仅误导,甚至可能掩盖严重的线上 Bug。

5.1.1. 改造靶场:模拟业务异常

我们需要修改 Spring Boot 代码,增加一个模拟库存扣减的接口,它会随机返回成功或失败。

文件路径src/main/java/com/demo/jmeterdemo/controller/OrderController.java

请新建 OrderController 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.demo.jmeterdemo.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

@RestController
@RequestMapping("/api/order")
public class OrderController {

/**
* 模拟下单接口
* 场景:30% 的概率因为 "库存不足" 而下单失败
* 注意:无论成功失败,HTTP 状态码都是 200
*/
@PostMapping("/create")
public Map<String, Object> create() {
Map<String, Object> result = new HashMap<>();
// 模拟 30% 的失败率
boolean isStockEnough = ThreadLocalRandom.current().nextInt(100) > 30;
if (isStockEnough) {
result.put("code", 200);
result.put("msg", "Success");
result.put("orderId", System.currentTimeMillis());
} else {
// 业务错误,但 HTTP 依然是 200
result.put("code", 5001);
result.put("msg", "Out of Stock");
}
return result;
}
}

重启项目,我们准备开始测试。

5.1.2. 观察 JMeter 的误判

  1. 在 JMeter 中新建线程组 " S02_断言测试 "。

  2. 添加 HTTP 请求,命名为 " API_下单 "。

配置如下参数:

  • Path: /api/order/create
  • Method: POST
  1. 设置线程组 循环次数10

  2. 运行并观察 察看结果树

结果分析:你会发现 10 次请求全部都是 绿色盾牌。点开响应数据,你会发现其中夹杂着 {"code": 5001, "msg": "Out of Stock"}

这就是 “假成功”。在真正的压测报告中,我们必须把这 30% 的库存不足标记为 “失败”,否则我们就不知道系统在什么并发量下会开始出现业务瓶颈。


5.2. 响应断言:最通用的门神

为了纠正 JMeter 的判断,我们需要添加 断言 (Assertion)。断言就是我们设定的 “通过标准”。

5.2.1. 添加响应断言

操作步骤

  1. 右键点击 " API_下单 " -> 添加 -> 断言 -> 响应断言 (Response Assertion)
  2. 配置以下参数:
参数项配置值解释
测试字段 (Apply to)Main sample only只检查主请求。
测试字段 (Field to Test)响应文本 (Text Response)检查 Response Body 的内容。
模式匹配规则包括 (Contains)只要包含指定字符串就算通过。
测试模式 (Patterns to Test)"code": 200点击【添加】按钮输入。这里我们强制要求返回结果必须包含这段 JSON 文本。
自定义失败消息业务代码非 200如果断言失败,结果树中会显示这句话,方便排查。

5.2.2. 验证效果

再次运行测试。

预期结果:在 察看结果树 中,你应该会看到大约 30% 的请求变成了 红色感叹号。点击红色的请求,展开 断言结果 (Assertion Failure Message),你会看到:
Assertion error: false that ... contains "code": 200

image-20251120103406076

这样,JMeter 的聚合报告中的 “Error %” 就能真实反映业务成功率了。


5.3. JSON 断言:结构化数据的专家

虽然 “响应断言” 简单好用,但通过字符串匹配 "code": 200 并不严谨。如果返回的 msg 里包含 code: 200 字符,可能会导致误判。

对于 JSON 接口,更推荐使用 JSON 断言

5.3.1. 配置 JSON 断言

我们先禁用掉刚才的 “响应断言”(右键 -> 禁用),添加一个新的断言。

操作步骤

  1. 右键点击 " API_下单 " -> 添加 -> 断言 -> JSON 断言 (JSON Assertion)
  2. 配置参数:
参数项配置值解释
Assert JSON Path exists$.orderId我们要求返回结果中必须包含 orderId 字段。只有下单成功才有这个字段。
Expected Value (可选)(空)如果勾选了 Additionally assert value,可以校验字段的值。这里我们只校验 “是否存在”。

原理:当返回 {"code": 5001, "msg": "Out of Stock"} 时,JSON 中没有 orderId 字段,断言通过 JSON Path 找不到路径,判定为失败。


5.4. 逻辑控制器:让脚本学会 “止损”

默认情况下,JMeter 像一个只会按顺序执行命令的机器人:无论上一步是成功还是炸了,它都会坚定地执行下一步。但在真实的业务场景中,如果用户连 “登录” 都失败了,后续的 “下单”、“支付” 操作根本就不应该发生。继续执行这些无效请求,不仅浪费压测机的 CPU,还会让服务端产生大量无意义的 401 错误日志,干扰问题排查。

我们需要利用 If 控制器 (If Controller) 来实现逻辑判断:只有当条件满足时,才执行内部的组件。

5.4.1. 场景重构:依赖链路

为了演示这个功能,我们需要构建一个典型的依赖场景:

  1. 前置动作:用户尝试登录。
  2. 判断依据:提取登录接口返回的状态码 code
  3. 分支逻辑
    • 如果 code == 200:执行 “API_下单”。
    • 如果 code != 200:直接跳过,不做任何操作。

准备工作:请确保你的 “API_登录” 请求下已经挂载了 JSON 提取器,并将状态码提取为变量 login_code

image-20251120105307483

5.4.2. 借助函数助手生成表达式

在配置 If 控制器之前,我们先解决最难的一步:如何写出正确的判断表达式?

手动编写 ${__jexl3(...)} 既容易错又难记。我们要利用 函数助手 自动生成代码。

操作步骤

  1. 点击 JMeter 顶部菜单栏的 工具 (Tools) -> 函数助手对话框 (Function Helper Dialog)

  2. 在左侧列表中找到并选中 __jexl3(这是 JMeter 高性能运算函数)。

  3. 在右侧的 参数值 栏位中,输入你的逻辑表达式:

    "${login_code}" == "200"

  4. 点击底部的 生成 (Generate) 按钮。

  5. 复制 生成的字符串:${__jexl3("${login_code}" == "200",)}

语法细节:为什么变量 ${login_code} 外面要加双引号?这是为了防止空指针。如果提取失败,变量为空,表达式会变成 "" == "200"(合法);如果不加引号,会变成 == "200"(语法错误)。

5.4.3. 配置 If 控制器

拿到生成的代码后,配置控制器就非常简单了。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 逻辑控制器 -> 如果 (If) 控制器
  2. 将 “API_下单” 请求 拖拽 到 “If 控制器” 的内部(使其成为子节点)。
  3. 在 If 控制器的面板中进行粘贴:
    • Expression:粘贴刚才复制的代码 ${__jexl3("${login_code}" == "200",)}
    • Interpret Condition as Variable Expression?必须勾选

始终勾选 “Interpret Condition as Variable Expression?”。这告诉 JMeter 直接使用高性能的 JEXL3 引擎处理变量,而不是启动笨重的 JavaScript 引擎。在高并发压测下,这一项配置能提升 10 倍以上的性能。

5.4.4. 验证 “止损” 效果

配置完成后,我们进行正反两面的测试,验证逻辑是否生效。

测试 A:正向用例(登录成功)

  1. 修改登录接口参数为正确的账号密码(admin/123456)。
  2. 运行脚本。
  3. 观察login_code 变为 200,你会在结果树中同时看到 “API_登录” 和 “API_下单”。

测试 B:反向用例(登录失败)

  1. 修改登录接口参数为错误的密码(如 admin/error)。
  2. 运行脚本。
  3. 观察
    • “API_登录” 执行,但业务码不是 200。
    • 关键点:在结果树中,完全看不到 “API_下单” 的记录
    • 这说明 If 控制器成功拦截了请求,脚本实现了智能止损。

image-20251120110600928


5.5. 本章小结

本章我们给脚本赋予了 “判断能力” 和 “决策能力”。

核心要点

  1. HTTP 200 不等于成功:压测必须基于业务指标,使用 响应断言JSON 断言 来修正成功率统计。
  2. 断言选择:简单的文本包含用响应断言;复杂的 JSON 字段校验用 JSON 断言。
  3. If 控制器:用于构建依赖链路,避免上游失败后下游继续无效执行,节省压测机资源。

下一步计划:目前我们的脚本已经非常健壮了,不仅能传参,还能自动判断对错。但是,它们还只能运行在标准 Java 代码无法解决的逻辑上。比如:“我想对密码进行 RSA 加密后再发送”,或者 “我想直接连数据库清理垃圾数据”。这些需求靠标准组件很难实现。在下一章,我们将解锁 JMeter 的核武器——JSR223 + Groovy 脚本编程


第六章. 效率与规范:配置元件与作用域详解

摘要:在之前的实战中,我们发现每次创建 HTTP 请求都要手动填写 IP 和端口,且经常因为组件位置放错导致提取失败。本章我们将引入 HTTP 请求默认值 来消除配置冗余,并深入剖析 JMeter 的 作用域(Scope)执行顺序,彻底揭开 “组件该放哪里” 的谜底。

本章学习路径

我们将掌握 JMeter 的高效设计模式:

  • 6.1.全局配置战术:一处修改,处处生效
    • 6.1.1 用户定义的变量:管理环境常量
    • 6.1.2 HTTP 请求默认值:统一连接参数
    • 6.1.3 HTTP 信息头管理器:统一通讯契约
  • 6.2 深入理解:JMeter 的执行顺序
    • 6.2.1 为什么调试取样器之前读不到变量?
    • 6.2.2 八大组件的生命周期图谱
  • 6.3 核心机制:作用域 (Scope)
    • 6.3.1 树状结构:父节点、子节点与兄弟节点
    • 6.3.2 HTTP 信息头管理器的合并策略
  • 6.4 状态自动管理:HTTP Cookie 管理器
    • 6.4.1 像浏览器一样自动处理 Session
    • 6.4.2 什么时候不需要手动提取 Token?

6.1. 全局配置战术:一处修改,处处生效

在之前的实战中,我们发现脚本存在大量的重复配置:每个请求都要填 IP,每个 POST 请求都要填 Header,每个接口都要加断言。这不仅繁琐,一旦后端接口规范调整(比如 Token 名字变了),你需要修改几十个地方。

本节我们将利用 配置元件 (Config Element) 搭建一套标准化的全局配置体系。

6.1.1. 用户定义的变量:管理环境常量

在第 3 章中,我们可能使用了“前置处理器”中的用户参数,但在全局配置场景下,更推荐使用 配置元件 中的 用户定义的变量 (User Defined Variables)。它在测试开始前就会初始化,且对整个线程组生效,性能更好。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 配置元件 -> 用户定义的变量
  2. 将其拖拽到线程组的 最顶部
  3. 添加以下常量:
名称描述
target_hostlocalhost目标服务器 IP
target_port8080目标端口
default_encodingUTF-8统一编码格式

6.1.2. HTTP 请求默认值:统一连接参数

有了变量后,我们需要配置一个“基站”,让所有 HTTP 请求自动继承这些参数。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 配置元件 -> HTTP 请求默认值
  2. 位置:放在“用户定义的变量”下方,所有 Sampler 上方。
  3. 配置参数
    • 协议http
    • 服务器名称或 IP${target_host}
    • 端口号${target_port}
    • 内容编码${default_encoding}

瘦身行动:现在,请打开你所有的 HTTP 请求(登录、下单等),把里面的 IP、端口、协议全部 清空。脚本瞬间清爽了!

6.1.3. HTTP 信息头管理器:统一通讯契约

前后端分离项目通常强制要求使用 JSON 交互。为了避免在每个 POST 请求里重复添加 Content-Type,我们做一次全局设定。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 配置元件 -> HTTP 信息头管理器
  2. 位置:与“默认值”平级。
  3. 添加标头
    • 名称Content-Type
    • application/json

效果:该线程组下的所有请求都会自动带上这个头。如果某个特殊接口(如文件上传)需要不同的类型,只需在该接口下再加一个信息头管理器,子节点会覆盖父节点

6.1.4. 全局响应断言:守住质量底线

你提到“不希望在每一个 API 接口下方添加验证”。的确,对于“HTTP 状态码必须为 200”这种通用标准,我们应该配置 全局断言

操作步骤

  1. 右键点击 线程组(注意是点击线程组,不是具体的请求) -> 添加 -> 断言 -> 响应断言
  2. 配置参数
    • 测试字段响应代码 (Response Code)
    • 模式匹配规则相等 (Equals)
    • 测试模式200

原理说明:因为这个断言是直接挂在 线程组 下面的(是所有请求的“叔叔/伯伯”节点,但在作用域逻辑上属于父级作用域),它会对线程组内 每一个 运行的请求生效。

  • 优点:一次配置,全员监控。只要有一个接口报 404 或 500,该请求就会被标记为失败。
  • 例外:如果某个接口(如“测试错误密码”)预期就是返回 401,你可以使用 作用域 规则,在该请求下添加一个独立的断言来覆盖或补充逻辑。

在最后我们确保一下我们的 JMeter 执行顺序如下:

image-20251120115742454


6.2. 深入理解:JMeter 的执行顺序

在第四章的调试过程中,很多初学者会遇到一个困惑:明明已经添加了提取器,为什么紧挨着的调试取样器读不到变量?

这通常是因为误解了 JMeter 的执行逻辑。JMeter 并不是简单地按照 “从上到下” 的视觉顺序执行,不同类型的组件有着严格的 优先级

6.2.1. 八大组件生命周期

任何一个 JMeter 取样动作的执行,都严格遵循以下时间轴(优先级从高到低):

  1. 配置元件 (Config Elements):最先执行。用于初始化环境(如读取 CSV、设置默认值)。
  2. 前置处理器 (Pre-Processors):在请求发送 执行。用于参数加工(如密码加密、生成签名)。
  3. 定时器 (Timers):在请求发送 执行等待。用于模拟思考时间。
  4. 取样器 (Samplers)核心动作。真正向服务器发送请求。
  5. 后置处理器 (Post-Processors):在请求结束 执行。用于提取响应数据(如 JSON 提取器)。
  6. 断言 (Assertions):在提取完成后执行。用于验证结果。
  7. 监听器 (Listeners):最后执行。用于记录和展示结果。

6.2.2. 案例复盘

让我们用这个规则来解释第四章的 “调试取样器 (Debug Sampler)” 问题。

错误场景

1
2
3
1. 调试取样器 (Debug Sampler)
2. API_老旧接口 (HTTP Request)
└── 正则表达式提取器 (Post-Processor)

执行流程分析

  1. JMeter 遇到 调试取样器(属于 Sampler)。由于它上面没有前置处理器,直接执行。此时," API_老旧接口 " 还没跑,提取器当然也没跑,所以变量不存在。
  2. JMeter 遇到 API_老旧接口(属于 Sampler),执行请求。
  3. 请求结束后,触发挂载的 正则表达式提取器(属于 Post-Processor),此时变量 legacy_sess 才被创建。

修正场景

1
2
3
1. API_老旧接口 (HTTP Request)
└── 正则表达式提取器 (Post-Processor)
2. 调试取样器 (Debug Sampler)

修正后的流程

  1. 先跑 " API_老旧接口 "。
  2. 跑完后触发提取器,生成变量。
  3. 再跑 调试取样器,此时它就能读取到内存中已存在的变量了。

6.3. 核心机制:作用域 (Scope)

除了 “时间顺序”,JMeter 还有 “空间范围”,也就是 作用域。这决定了组件对哪些请求生效。JMeter 的测试计划是一个 树状结构,遵循 “父子继承,兄弟隔离” 的规则。

6.3.1. 树状法则

我们以 HTTP 信息头管理器(用于设置 Content-Type)为例:

场景 A:全局生效(父节点作用域)

1
2
3
4
Thread Group
├── HTTP 信息头管理器 (Content-Type: application/json)
├── API_登录
└── API_下单
  • 效果:该管理器是两个 API 的 兄弟节点(但在逻辑上属于线程组的子节点,作用于线程组内所有 Sampler)。因此,“登录” 和 “下单” 都会自动带上 JSON 头。

场景 B:局部生效(子节点作用域)

1
2
3
4
Thread Group
├── API_登录
│ └── HTTP 信息头管理器 (Content-Type: application/json)
└── API_下单
  • 效果:该管理器是 " API_登录 " 的 子节点。它只对 " 登录 " 生效。" 下单 " 接口不会携带该请求头。

6.3.2. 合并策略 (Merge)

如果父节点配置了 Header,子节点也配置了 Header,会发生什么?

规则

  1. 不同 Key:累加。
  2. 相同 Key子节点覆盖父节点

示例

  • 全局配置Authorization: None
  • 局部配置(在 " API_下单 " 下):Authorization: Bearer xyz
    • 最终结果*:“API_下单” 发送时,Authorization 的值为 Bearer xyz

在之前的章节中,我们通过 JSON 提取器手动获取 Token 并传递。这适用于现代的 JWT(无状态)架构。但对于传统的 Web 应用(如基于 JSP、Thymeleaf 的 Spring Boot 项目),服务器通常使用 JSESSIONID Cookie 来识别用户。

对于这种场景,JMeter 提供了一个 “作弊神器”,能让它像浏览器一样自动管理 Session。

6.4.1. 像浏览器一样工作

浏览器有一个特性:一旦服务器返回了 Set-Cookie 响应头,浏览器会自动保存,并在访问同一个域名的后续请求中自动带上 Cookie 头。

JMeter 默认是 无状态 的(不保存 Cookie)。要开启这个功能,只需添加一个组件。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 配置元件 -> HTTP Cookie 管理器
  2. 配置:通常保持默认即可(它会自动遵循标准的 Cookie 策略)。
  3. 位置:建议放在 线程组顶部,使其对所有请求生效。

6.4.2. 效果验证

一旦添加了 HTTP Cookie 管理器,你就不再需要编写正则表达式去提取 JSESSIONID 了。

工作流程

  1. API_登录 执行,响应头包含 Set-Cookie: JSESSIONID=A1B2...
  2. Cookie 管理器 自动捕获并将其存储在当前线程的内存中。
  3. API_下单 执行时,Cookie 管理器自动检测到目标域名匹配,将 Cookie: JSESSIONID=A1B2... 注入请求头。

如果你的应用是前后端分离且使用 Token (Header) 鉴权:使用 JSON 提取器 + 信息头管理器

如果你的应用是传统 Web 且使用 Cookie/Session 鉴权:使用 HTTP Cookie 管理器


6.5. 本章小结

本章我们从 “写脚本” 进化到了 “设计脚本”,解决了很多初学者 “懂发请求但不懂配置” 的痛点。

核心要点

  1. DRY 原则:善用 HTTP 请求默认值,避免在几十个接口中重复修改 IP 和端口。
  2. 执行流水线:牢记 “配置 -> 前置 -> 动作 -> 后置 -> 断言” 的生命周期,这是排查 “变量取不到” 或 “断言失效” 的根本依据。
  3. 作用域法则
    • 想要全局生效,就放在线程组下一级。
    • 想要局部生效,就挂在具体请求的下面。
    • 子节点的配置会覆盖父节点(同名覆盖,异名累加)。
  4. Cookie 管理器:测试传统 Web 应用时,它是自动处理 Session 的神器,能节省大量提取代码。

下一步计划:现在的脚本结构已经非常规范了。但目前的压测行为更像是 “机器人”——以固定的频率、没有任何停顿地发送请求。而真实的用户在点击之前会有 “思考时间”,在抢购时会有 “瞬间并发”。在下一章,我们将引入 定时器 (Timers),让压测流量无限逼近真实世界。


第七章. 流量仿真:定时器与高阶控制器

摘要:在前面的章节中,我们的脚本像一个不知疲倦的机器人,以毫秒级的速度连续发送请求。但这与真实用户的行为背道而驰,且无法测试出“线程安全”等深层问题。本章我们将引入 定时器 来还原用户的“思考时间”,利用 同步定时器 制造绝对并发来检测 Spring Boot 的锁机制,并通过 高阶控制器 封装复杂的业务逻辑。

本章学习路径

我们将从“仿真”的角度出发,把脚本从“发包工具”升级为“用户模拟器”:

  • 7.1 还原真实用户:定时器 (Timers)
    • 7.1.1 并发数 $\neq$ 压力:思考时间的重要性
    • 7.1.2 统一随机定时器:模拟自然波动
  • 7.2 制造绝对洪峰:同步定时器
    • 7.2.1 压力测试 vs 并发测试的区别
    • 7.2.2 集合点实战:击穿数据库库存
    • 7.2.3 避免死锁:Timeout 参数的最佳实践
  • 7.3 业务视角封装:事务控制器
    • 7.3.1 技术指标 vs 业务指标
    • 7.3.2 “Generate parent sample” 深度解析
  • 7.4 复杂逻辑编排:循环控制器
    • 7.4.1 场景:轮询支付状态
    • 7.4.2 线程循环 vs 控制器循环的区别

7.1. 还原真实用户:定时器 (Timers)

在上一章中,我们解决了配置冗余的问题。但在实际运行中,你可能会发现:明明设置了 100 个线程,为什么服务器的 CPU 瞬间就飙升到 100% 然后报错?而生产环境 1000 个在线用户却很平稳?

这是因为你忽略了 “思考时间” (Think Time)

7.1.1. 核心概念:并发数与 TPS 的关系

在性能测试领域,有一个著名的公式:
$$TPS = \frac{并发用户数}{响应时间 + 思考时间}$$

  • 机器人的行为:响应时间 100ms,思考时间 0ms。
    $$TPS = 1 / 0.1 = 10$$ (单线程每秒发 10 个请求)
  • 真实用户的行为:响应时间 100ms,思考时间 3000ms(用户看完页面再点)。
    $$TPS = 1 / 3.1 \approx 0.3$$ (单线程每 3 秒才发 1 个请求)

结论:如果不加定时器,100 个 JMeter 线程产生的压力,可能相当于 3000 个真实用户的压力。为了让测试结果具备参考价值,我们必须模拟这种“停顿”。

7.1.2. 实战:统一随机定时器 (Uniform Random Timer)

JMeter 提供了“固定定时器”,但在现实中,没有哪两个用户的思考时间是完全一秒不差的。为了模拟更自然的流量波动,我们推荐使用 统一随机定时器

场景设计:用户登录成功后,浏览商品详情页,随机停留 1~3 秒,然后点击下单。

操作步骤

  1. 展开 API_下单 请求。
  2. 右键点击 API_下单 -> 添加 -> 定时器 -> 统一随机定时器
  3. 配置参数
    • Random Delay Maximum (随机延迟最大值): 2000
    • Constant Delay Offset (固定延迟偏移): 1000

原理解析
$$总等待时间 = 固定偏移 + random(0, 随机最大值)$$
代入数值:$1000 + [0, 2000] = 1000ms \sim 3000ms$。这样的设置既保证了用户至少会看 1 秒(固定值),又模拟了手速的快慢差异(随机值)。


7.2. 制造绝对洪峰:同步定时器

在上一节,我们通过随机定时器让流量变得平滑。但在某些特殊场景——例如“秒杀”、“抢红包”——我们需要反其道而行之,制造瞬间的压力洪峰。

7.2.1. 压力测试 vs 并发测试

  • 压力测试:考察系统在高负载下的稳定性(如 CPU 是否打满,GC 是否频繁)。
  • 并发测试:考察系统对 共享资源 的抢占处理(如数据库锁、线程安全)。

普通的 JMeter 压测,线程是陆续启动的,很难在 微秒级 做到绝对同时请求。如果你的 Spring Boot 代码中有 stock = stock - 1 这种非原子操作,普通压测可能测不出 Bug,但上线就会超卖。

这时我们需要 同步定时器 (Synchronizing Timer),也就是 LoadRunner 中的 集合点 (Rendezvous Point)

7.2.2. 实战:击穿库存

假设我们要测试 Spring Boot 的库存扣减逻辑。

操作步骤

  1. API_下单 的子节点下,添加 同步定时器
  2. 配置参数
    • Number of Simulated Users to Group by: 50
    • Timeout in milliseconds: 3000

执行逻辑

  1. 线程启动后,运行到“下单”步骤时,会被定时器拦截并挂起。
  2. JMeter 会一直等待,直到积攒了 50 个被挂起的线程。
  3. 一旦达到 50 个,定时器瞬间释放,50 个请求在同一毫秒内涌向服务器。
  4. 这就构成了对数据库行锁的 绝对并发竞争

7.2.3. 避免死锁:Timeout 的重要性

场景演示:假设你的线程组只设置了 30 个线程,但同步定时器要求集合 50 人。

  • 结果:前 30 个线程到达集合点后开始等待第 31 人,但永远等不到。脚本会陷入 无限等待(死锁),进度条永远卡住。

最佳实践:永远不要把 Timeout 设置为 0(0 代表无限等待)。务必设置一个合理的超时时间(如 3000ms)。如果 3 秒内凑不齐 50 人,JMeter 会强制释放已到达的线程,继续执行后续步骤,避免脚本卡死。


7.3. 业务视角封装:事务控制器

在 JMeter 的默认报告中,我们看到的都是单个接口的耗时(登录 50ms,下单 80ms)。但产品经理通常会问:“用户完成一次购买流程需要多久?”。

简单的相加是不准确的,因为中间可能包含重定向、定时器等待等时间。我们需要 事务控制器 (Transaction Controller)

7.3.1. 配置事务

操作步骤

  1. 右键点击 线程组 -> 添加 -> 逻辑控制器 -> 事务控制器
  2. API_登录API_下单(及其附属组件)全部拖拽到事务控制器内部。
  3. 关键配置
    • Generate parent sample (生成父样本)必须勾选
    • Include duration of timer (包含定时器耗时):根据需求勾选。
      • 如果测的是 用户体验:勾选(用户觉得卡顿是包含了思考时间的)。
      • 如果测的是 系统处理能力:不勾选(只统计服务器纯处理时间)。

7.3.2. 数据解读

运行测试后,查看聚合报告。

  • 未勾选 Generate parent sample:你会看到三个条目——“API_登录”、“API_下单”、“事务控制器”。数据比较杂乱。
  • 勾选 Generate parent sample:你会看到一个合并后的条目 “事务控制器”,原来的子接口被隐藏了。此时的 TPS 和 RT 指标,反映的就是完整的“购买业务”的处理能力。

image-20251120143052508


7.4. 复杂逻辑编排:循环控制器

并不是所有的业务都是“线性”的(登录 -> 下单 -> 结束)。在支付场景中,往往存在“轮询”机制:前端每隔 1 秒查询一次后端状态,直到支付成功或超时。

7.4.1. 实战:轮询支付状态

我们需要在脚本中模拟:“下单成功后,每隔 1 秒查询一次订单状态,共查询 5 次”。

操作步骤

  1. 线程组 中添加 逻辑控制器 -> 循环控制器 (Loop Controller)
  2. 配置参数
    • Loop Count (循环次数)5
  3. 在循环控制器内部添加:
    • HTTP 请求GET /api/order/status
    • 固定定时器1000ms

7.4.2. 线程循环 vs 控制器循环

这是初学者最容易混淆的概念:

  • 线程组的 Loop Count:决定了 整个剧本 演多少遍。
    • 如果设为 10,意味着“登录 -> 下单 -> 轮询”这全套流程做 10 遍。
  • 循环控制器的 Loop Count:决定了 剧本中某一个小节 重复多少遍。
    • 如果设为 5,意味着在每一遍剧本中,“查询状态”这个动作要重复 5 次。

通过组合使用,我们可以构建出非常复杂的业务模型:1次登录 -> 5次浏览 -> 1次下单 -> 3次查询状态


7.5. 本章小结

本章我们为脚本注入了“灵魂”,使其从简单的接口调用工具进化为复杂的用户行为模拟器。

核心要点

  1. 思考时间:必须使用 定时器 模拟用户停顿,否则压测结果中的 TPS 会虚高,无法代表真实负载。
  2. 作用域:定时器挂在谁下面,就只影响谁。切忌在线程组层级随意添加定时器。
  3. 绝对并发:使用 同步定时器 模拟秒杀场景,这是检测 Spring Boot 线程安全问题的杀手锏,但切记设置 Timeout 防止死锁。
  4. 事务统计:使用 事务控制器 聚合多个接口,勾选 Generate parent sample 获取清晰的业务级性能报告。

速查配置

  • 统一随机定时器:Offset = 固定等待,Max = 随机波动。
  • 同步定时器:Timeout 不要设为 0。

下一步计划:至此,我们已经彻底掌握了 JMeter 的 GUI 组件(配置、请求、断言、定时器、控制器)。但在面对一些极度复杂的场景(如:RSA 动态签名、自定义 Redis 操作、复杂的数据清洗)时,GUI 界面已经无法满足需求了。下一章,我们将解锁 JMeter 的终极能力——Groovy 脚本编程,真正实现“为所欲为”的测试。


第八章. JSR223 与 Groovy:JMeter 的核武器

摘要:在前面的章节中,我们通过鼠标点选 GUI 组件完成了大部分任务。但真实业务往往比这复杂得多:比如接口需要 “MD5 加密签名”、需要 “AES 解密响应”、或者需要把数据 “写入本地 Excel”。面对这些需求,GUI 界面束手无策。本章我们将解锁 JMeter 的 JSR223 组件配合 Groovy 语言,让你拥有直接编写代码操控压测逻辑的能力。

本章学习路径

我们将从面板认知开始,一步步掌握脚本编程:

  • 8.1 认识 JSR223 组件
    • 8.1.1 JSR223 是什么?为什么不是 BeanShell?
    • 8.1.2 面板功能区详解(入口与配置)
  • 8.2 Groovy 语言速成(面向 Java 开发者)
    • 8.2.1 为什么它是 JMeter 的御用语言
    • 8.2.2 核心语法差异:丢掉分号与类型
  • 8.3 JMeter 的四大内置对象
    • 8.3.1 log:你的调试眼睛
    • 8.3.2 vars:变量的搬运工
    • 8.3.3 propsctx:跨线程与上下文(了解)
  • 8.4 实战:手写 MD5 加密前置处理器
    • 8.4.1 引入 Java 工具类
    • 8.4.2 编写并调试加密脚本
  • 8.5 实战:自定义数据写入后置处理器
    • 8.5.1 文件流操作
    • 8.5.2 将订单号落盘保存

8.1. 认识 JSR223 组件

在 “添加” 菜单中,你会发现很多带有 “脚本” 字样的组件,最著名的就是 BeanShellJSR223。请记住一句话:在 2025 年,请彻底遗忘 BeanShell,只用 JSR223 + Groovy。

8.1.1. 核心概念:Interface vs Language

  • JSR223:这是一个 Java 规范(Java Specification Request 223),它定义了一个标准接口,允许 Java 程序调用各种脚本语言(如 Python, Ruby, Groovy)。在 JMeter 中,它是一个 容器
  • Groovy:这才是我们要写的 语言。它完全兼容 Java 语法,但更简洁。
  • 为什么要用它?
    • BeanShell 是解释执行的,性能极差(并发一高就会卡死)。
    • Groovy 支持 “编译缓存”,JMeter 会把它编译成原生的 .class 字节码运行,性能几乎等同于原生 Java。

8.1.2. 面板功能区详解

让我们先找到并打开这个组件。

操作步骤

  1. 右键点击 线程组 -> 添加 -> 取样器 (Sampler) -> JSR223 取样器
  2. 你会看到如下界面,我们需要关注三个核心区域:

关键配置项说明

  1. Language (语言)
    • 必须选择 groovy。千万不要选 javabeanshell,否则无法享受性能优化。
  2. Cache compiled script if available (缓存编译脚本)
    • 必须勾选。这是性能起飞的关键。勾选后,这段代码只会被编译一次,之后 100 万次循环都直接运行机器码。
  3. Script (脚本编辑区)
    • 这里就是我们写代码的地方。虽然它像个记事本,没有代码提示,但它支持标准的 Java/Groovy 语法。

8.2. Groovy 语言速成

对于已经掌握 Spring Boot 的你来说,学习 Groovy 是 “零成本” 的。因为 任何合法的 Java 代码都是合法的 Groovy 代码

你可以直接在脚本区写 System.out.println("Hello");,它是能跑的。但 Groovy 提供了一些 “语法糖”,让代码更简洁。

8.2.1. 核心语法差异表

特性Java 写法Groovy 写法 (推荐)优势
分号String name = "Jack";String name = "Jack"可以省略分号,代码更干净。
类型定义String id = "123";def id = "123"使用 def 自动推断类型,类似 JS 的 let。
字符串插值"ID is " + id"ID is ${id}"使用双引号 + ${} 直接拼接变量。
Get/Setuser.getName()user.name自动调用 getter/setter 方法。

建议:作为初学者,为了避免出错,你完全可以 直接写标准的 Java 代码。等你熟练了,再尝试 Groovy 的简化写法。


8.3. JMeter 的四大内置对象

在 Script 编辑区写代码时,JMeter 已经默默地往这一小块空间里注入了几个 “上帝对象”。你不需要 new,直接就能用。

8.3.1. log:你的调试眼睛

在 GUI 界面写代码没有断点调试,我们只能靠打印日志来观察变量。

  • 代码
    1
    2
    log.info("这是普通信息");
    log.error("这是报错信息");
  • 查看位置:点击 JMeter 界面右上角的 黄色感叹号图标,底部会弹出一个控制台窗口,你的日志就显示在那里。

8.3.2. vars:变量的搬运工(最重要)

varsJMeterVariables 类的实例。它连接了 GUI 组件代码世界

  • 读取变量(从 GUI -> 代码):假设你在 “用户定义的变量” 中定义了 target_host

    1
    String ip = vars.get("target_host"); // 获取变量值
  • 写入/修改变量(从 代码 -> GUI):假设你想把计算好的结果传给下一个 HTTP 请求。

    1
    vars.put("new_token", "xwq89-sdsd-223"); // 创建名为 new_token 的变量

8.4. 实战 A:前置处理器 - 攻克 MD5 签名校验

在真实的企业级开发中,为了防止请求被篡改,后端往往要求前端对核心参数进行加密签名。例如:注册接口要求密码必须传输 32 位 MD5 密文,如果传输明文直接报错。

JMeter 的 GUI 组件没有自带 MD5 加密功能,这时候就轮到 JSR223 前置处理器大显身手了。

8.4.1. 第一步:改造靶场(制造困难)

为了模拟这个场景,我们需要先在 Spring Boot 项目中增加一个“强制校验 MD5”的注册接口。

文件路径src/main/java/com/demo/jmeterdemo/controller/AuthController.java

请在 AuthController 类中追加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 4. 模拟高安全级别的注册接口
* 规则:password 字段严禁传输明文,必须是 32 位的 MD5 字符串
*/
@PostMapping("/register")
public Map<String, Object> register(@RequestBody Map<String, String> user) {
Map<String, Object> result = new HashMap<>();
String password = user.get("password");

// 模拟后端校验:如果长度不是 32 位,说明不是标准的 MD5,直接拒绝
if (password == null || password.length() != 32) {
result.put("code", 400);
result.put("msg", "SecurAity Error: Password must be MD5 encrypted!");
return result;
}

// 校验通过
result.put("code", 200);
result.put("msg", "Register Success");
result.put("username", user.get("username"));
return result;
}

操作提醒

  1. 粘贴代码后,请重启 Spring Boot 项目。
  2. 确保控制台无报错,端口 8080 正常监听。

8.4.2. 第二步:遭遇失败(复现问题)

我们先尝试用常规方式去请求,看看会发生什么。

  1. 在 JMeter 线程组下新建一个 HTTP 请求,命名为 " API_注册 "
  2. Method: POST
  3. Path: /api/auth/register
  4. Body Data:
    1
    2
    3
    4
    {
    "username": "admin",
    "password": "123456"
    }
  5. 运行测试,查看 察看结果树
    • 响应结果{"code":400, "msg":"Security Error: Password must be MD5 encrypted!"}
    • 分析:后端校验生效,传输明文 “123456” 被拒绝。我们需要在发送请求 之前,把 “123456” 变成 MD5 密文。

8.4.3. 第三步:脚本编程(JSR223 救场)

我们需要使用 前置处理器 (PreProcessor),它的执行时机是在 HTTP 请求发送 之前

操作流程

  1. 修改 Body:将明文密码替换为变量占位符。

    1
    2
    3
    4
    {
    "username": "admin",
    "password": "${md5_pwd}"
    }

    (注:变量 ${md5_pwd} 目前还不存在,我们马上用代码生成它)

  2. 添加组件:右键点击 " API_注册_成功 " -> 添加 -> 前置处理器 -> JSR223 预处理程序

  3. 配置面板

    • 语言:选择 groovy(必须!)。
    • 缓存:勾选 Cache compiled script if available

编写 Groovy 脚本

请在 Script 编辑区输入以下代码。这段代码利用了 JMeter 自带的 commons-codec 库,这是 Java 处理加密的标准姿势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 导入加密工具类 (JMeter 自带,无需下载 jar 包)
import org.apache.commons.codec.digest.DigestUtils

// 2. 定义原始明文密码
String rawPass = "123456";

// 3. 执行 MD5 加密A
// md5Hex 是静态方法,会将字符串转换为 32 位小写 Hex 字符串
String encryptedPass = DigestUtils.md5Hex(rawPass);

// 4. 【调试关键】打印日志
// 这一步非常重要,如果不打日志,出错了你都不知道是算错了还是没传过去
log.info("========== MD5 加密开始 ==========");
log.info("原始密码: " + rawPass);
log.info("加密结果: " + encryptedPass);
log.info("=================================");

// 5. 【核心动作】将结果存入 JMeter 变量池
// "md5_pwd" 对应我们在 Body 中写的 ${md5_pwd}
vars.put("md5_pwd", encryptedPass);

8.4.4. 第四步:全链路验证(闭环检查)

脚本写好了,能不能跑通?我们需要检查三个地方。

操作步骤

  1. 打开日志监视器:点击 JMeter 右上角的黄色感叹号图标(或菜单栏 选项 -> 日志查看器),清空旧日志。
  2. 运行脚本:点击启动按钮。
  3. 检查点 1:看日志
    • 观察下方控制台,是否输出了 加密结果: e10adc3949ba59abbe56e057f20f883e
    • 如果有,说明 Groovy 代码运行正常,加密逻辑成功。
  4. 检查点 2:看请求体 (Request Body)
    • 察看结果树 中选中 " API_注册_成功 "。
    • 点击 请求 (Request) 选项卡 -> Request Body。
    • 观察 password 字段:"password": "e10adc3949ba59abbe56e057f20f883e"
    • 说明 vars.put 生效了,变量成功替换了占位符。
  5. 检查点 3:看响应 (Response)
    • 点击 响应数据 (Response Data)
    • 看到 {"code":200, "msg":"Register Success"}
    • 说明后端校验通过。

通过这四步,我们完整实现了一个 “Java 加密 -> JMeter 变量 -> HTTP 请求” 的数据流转。


8.5. 实战 B:后置处理器 - 核心数据落盘保存

在压测过程中,我们经常需要把生产出来的数据(比如:注册成功的用户名、下单成功的订单号)保存下来,作为下一轮压测的输入数据,或者发给其他部门进行对账。

JMeter 的 “保存响应到文件” 组件功能很弱(只能存整个响应),要想灵活地只存一个 ID,必须使用 JSR223 后置处理器

8.5.1. 第一步:确认数据源

我们要保存的是 下单接口 返回的 orderId

  1. 确保你已经有了 " API_下单 " 接口(参考第 5 章)。
  2. 确保该接口下挂载了 JSON 提取器
    • 变量名称: orderId
    • JSON 路径: $.orderId

8.5.2. 第二步:编写落盘脚本

我们需要在提取出 orderId 之后,把它写入电脑的硬盘里。

操作步骤

  1. 右键点击 " API_下单 " -> 添加 -> 后置处理器 -> JSR223 后置处理程序
    • 注意顺序:它必须放在 “JSON 提取器” 的 下方(因为要先提取,再写入)。
  2. 配置面板:语言选 groovy,勾选缓存。

编写 Groovy 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. 从 JMeter 变量池获取 OrderID
// 这个变量是由上面的 JSON 提取器生成的
String id = vars.get("orderId");

// 2. 安全校验:如果提取失败,绝对不能写入
// 否则文件里会有一堆 "null" 或默认值,导致数据污染
if (id == null || id.equals("null")) {
log.error(">>> 严重警告:未能获取到订单ID,跳过写入操作!");
return; // 直接终止当前脚本,不执行后面的写入
}

// 3. 定义文件路径
// 建议使用正斜杠 /,它在 Windows 和 Mac/Linux 上都能通用
// 请确保 D 盘存在,或者修改为你电脑上存在的路径(如 /tmp/orders.csv)
String filePath = "D:/jmeter_orders.csv";

try {
// 4. 创建文件写入流
// 参数 true 表示 "Append Mode" (追加模式)
// 如果设为 false,每次运行都会清空旧文件,只存最后一条,这通常不是我们想要的
FileWriter fwriter = new FileWriter(filePath, true);
BufferedWriter out = new BufferedWriter(fwriter);

// 5. 写入数据并换行
out.write(id);
out.write("\n"); // 必须换行,否则所有 ID 会连成一长串

// 6. 关闭流 (极其重要!)
// 如果不关闭,数据可能卡在缓冲区里,文件里就是空的
out.close();
fwriter.close();

// 打印成功日志,方便确认
log.info("成功保存订单ID: " + id);

} catch (Exception e) {
// 如果文件被占用或没有权限,打印错误堆栈
log.error("写入文件失败: " + e.getMessage());
}

8.5.3. 第三步:验证数据落盘

代码写得再漂亮,文件里有数据才是硬道理。

验证流程

  1. 清理环境:如果 D:/jmeter_orders.csv 已经存在,建议先手动删除它,确保我们看到的是新的。
  2. 运行脚本:设置线程组循环 5 次,点击启动。
  3. 观察 JMeter
    • 查看日志窗口,应该有 5 条 成功保存订单ID: xxxx 的记录。
    • 确保没有红色的 写入文件失败 报错。
  4. 检查硬盘文件
    • 打开 D:/ 盘(或你设置的路径)。
    • 找到 jmeter_orders.csv,用记事本打开。
    • 预期结果:应该看到 5 行不同的数字 ID。
1
2
3
4
1731988888123
1731988889456
1731988890789
...

如果能看到这个文件,恭喜你,你已经掌握了用 JMeter 处理复杂数据流的核心技能。


8.6. 本章小结

本章我们跨越了 GUI 的边界,进入了代码的领域。这是从中级测试工程师迈向高级的关键一步。

核心要点

  1. 工具链:坚决使用 JSR223 + Groovy,配合 Cache 选项,性能是 BeanShell 的百倍。
  2. 调试法:脚本是看不见摸不着的,必须依赖 log.info() 打印关键变量,通过日志控制台来 “透视” 运行过程。
  3. 数据流
    • GUI -> 代码vars.get("key")
    • 代码 -> GUIvars.put("key", "value")
  4. 安全性:在进行文件读写等高危操作时,务必进行 判空校验 (null check)异常捕获 (try-catch),防止因为一条脏数据导致整个测试中断。

第九章. 可视化监控体系:InfluxDB + Grafana

摘要:在之前的测试中,我们一直依赖 JMeter 自带的 GUI 监听器(如聚合报告)查看结果。这种方式有两个致命缺点:一是 GUI 消耗资源大,不适合高并发;二是无法实时查看 TPS 趋势图。本章我们将搭建 JMeter + InfluxDB + Grafana 黄金链路,抛弃丑陋的静态报告,打造好莱坞大片级别的实时监控大屏。

本章学习路径

我们将按照 “数据生产 -> 数据存储 -> 数据展示 -> 数据解读” 的闭环进行掌握:

  • 9.1 监控架构演进
    • 9.1.1 为什么要抛弃 GUI 报告?
    • 9.1.2 JIG 架构解析 (JMeter + InfluxDB + Grafana)
  • 9.2 部署监控基础设施
    • 9.2.1 方案 A:Docker Compose 一键部署(推荐)
    • 9.2.2 方案 B:Windows 原生安装
  • 9.3 配置 JMeter 后端监听器
    • 9.3.1 Backend Listener 核心配置
    • 9.3.2 关键参数:summaryOnlypercentiles
  • 9.4 配置 Grafana 大屏
    • 9.4.1 数据源配置
    • 9.4.2 导入官方经典模板 (ID: 5496)
  • 9.5 深度解读:像医生一样看大屏 (新增)
    • 9.5.1 顶部导航与全局概览:掌握压测节奏
    • 9.5.2 核心指标仪表盘:一秒判断生死
    • 9.5.3 趋势图与错误分析:定位性能拐点

9.1. 监控架构演进

9.1.1. 痛点分析

在此前的章节中,我们依靠 “聚合报告” 看数据。但在真实压测中,它有三大罪状:

  1. 资源黑洞:GUI 监听器会将所有结果保存在内存中。如果压测持续 1 小时,积攒的百万条数据会直接把 JMeter 客户端撑爆(OOM)。
  2. 后知后觉:你只能在压测结束后看到平均值,无法看到压测过程中的 “抖动”(比如第 5 分钟突然发生了 TPS 暴跌)。
  3. 数据孤岛:无法与服务器的 CPU/内存监控图表放在一起对比。

9.1.2. JIG 架构解析

为了解决上述问题,业界通用的方案是 JIG

  1. JMeter (生产者):负责施压。它不再把数据留在内存,而是通过 Backend Listener 异步地把精简后的统计数据 “推” 出去。
  2. InfluxDB (存储者):一个高性能的 时序数据库,专门用来存这种带时间戳的监控数据。
  3. Grafana (展示者):一个颜值极高的数据可视化平台,从 InfluxDB 读数据,画成炫酷的图表。

9.2. 部署监控基础设施

为了降低学习成本,我们推荐使用 Docker 快速搭建。如果你没有 Docker 环境,也可以选择原生安装,如果您不熟悉 docker,可转至

9.2.1. 方案 A:Docker Compose 一键部署

如果你是 Spring Boot 开发者,本地应该有 Docker Desktop。

操作步骤

  1. 在任意目录创建一个 docker-compose.yml 文件。
  2. 粘贴以下内容(使用 InfluxDB 1.8 版本,因为 JMeter 对 1.x 协议支持最原生,配置最简单):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3'
services:
influxdb:
image: influxdb:1.8
container_name: jmeter-influxdb
ports:
- "8086:8086"
environment:
- INFLUXDB_DB=jmeter
networks:
- monitoring

grafana:
image: grafana/grafana:latest
container_name: jmeter-grafana
ports:
- "3000:3000"
depends_on:
- influxdb
networks:
- monitoring

networks:
monitoring:
  1. 在终端执行命令:
    1
    docker-compose up -d
  2. 验证:访问 http://localhost:3000 能看到 Grafana 登录页(默认账号密码 admin/admin),说明部署成功。

9.2.2. 方案 B:Windows 原生安装(备选)

如果不使用 Docker,你需要分别下载软件。

  1. InfluxDB:下载 v1.8.10 windows 二进制包 -> 解压 -> 运行 influxd.exe
  2. Grafana:下载 Windows Installer -> 安装 -> 启动服务。

(注:为了教学流畅性,后续演示基于 Docker 环境,端口均为默认)


9.3. 配置 JMeter 后端监听器

基础设施搭建完毕,现在要配置 JMeter 往数据库里 “推” 数据。

9.3.1. 添加 Backend Listener

操作步骤

  1. 打开你的 JMeter 脚本(建议使用包含 " API_登录 " 和 " API_下单 " 的脚本)。
  2. 右键点击 线程组 -> 添加 -> 监听器 -> 后端监听器 (Backend Listener)
  3. 位置:放在线程组的最下方,或者测试计划的最外层(监听所有线程组)。

9.3.2. 核心参数配置

在后端监听器面板中,请依次完成以下核心参数的配置:

  1. Backend Listener implementation
1
org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient

选择 InfluxDB 协议客户端,这是连接的基础。

  1. influxdbUrl
1
http://localhost:8086/write?db=jmeter

数据写入地址。其中 db=jmeter 对应我们在 Docker 中预先创建的数据库名称。

  1. application
1
SpringBoot_App_Test

应用名称。该字段用于标识当前的压测项目,后续在 Grafana 中可以通过这个名字筛选出特定项目的监控数据。

  1. measurement
1
jmeter

InfluxDB 中的表名(Measurement),通常保持默认即可。

  1. summaryOnly
1
false

关键配置! 该选项默认为 false, 他使得 JMeter 记录每一个 Request 的详细数据,Grafana 才能依据这些数据绘制出精细的明细图表。

  1. percentiles
1
90;95;99

配置我们关心的百分位性能指标(如 TP90、TP95、TP99)。

验证配置

点击启动 JMeter。虽然界面上看不到任何弹窗提示,但此时 JMeter 已经开始默默地通过 UDP/HTTP 协议向 localhost:8086 发送数据包了。

9.4. 配置 Grafana 大屏

最后一步,把数据画出来。

9.4.1. 配置数据源 (Data Source)

  1. 浏览器打开 http://localhost: 3000 ,登录 Grafana,用户名: admin,密码: admin
  2. 点击左侧齿轮图标 -> Data Sources -> Add data source
  3. 选择 InfluxDB
  4. 配置详情
    • URL: http://influxdb:8086 (如果你用 Docker) 或 http://localhost:8086 (如果你是原生安装)。
    • Database: jmeter
  5. 点击底部 Save & Test。如果显示绿色的 “Data source is working”,说明连接成功。

image-20251120152622352

9.4.2. 导入炫酷模板

我们不需要自己一个一个画图,社区已经有大神做好了完美的模板。

  1. 点击 Grafana 左侧加号图标 -> Import
  2. Import via grafana.com 输入框中,填入 ID:5496
    • (注:模板 5496 是最经典的 JMeter Dashboard,由 Apache 官方推荐)
  3. 点击 Load
  4. 在底部的 DB name 下拉框中,选择刚才创建的 InfluxDB 数据源。
  5. 点击 Import

9.4.3. 实战:见证奇迹的时刻

现在,屏幕上应该出现了一个空的大屏。让我们让它动起来。

image-20251120153141779

  1. 回到 JMeter
  2. 调整线程组:设置 50 个线程,循环 “永远”(或设置一个很大的循环次数),持续运行 5 分钟。
  3. 点击 启动
  4. 回到 Grafana 浏览器页面,右上角选择刷新频率为 5s

你将看到:

  • Total Requests:请求数像里程表一样疯狂跳动。
  • Active Users:显示当前在线的 50 个用户。
  • Response Times (TR):平滑的曲线展示着 TP99 和 TP90 的波动。
  • Throughput (TPS):绿色的线代表成功 TPS,红色的线代表失败 TPS。

最佳实践:在这一刻,你可以把 “察看结果树” 和 “聚合报告” 都禁用了。在大规模压测中,我们只需要盯着 Grafana 的这个大屏,就能掌握一切。


9.5. 深度解读:像医生一样看大屏

大屏跑起来了,但如果看不懂数据的含义,它就只是一张漂亮的壁纸。为了精准定位性能瓶颈,我们需要学会正确地使用 Grafana 的分区功能。

我们将遵循 “顶层筛选 -> 局部诊断 -> 异常排查 -> 全局总结” 的逻辑,带你读懂每一个像素。

9.5.1. 驾驶舱:顶栏与折叠技巧

首先看屏幕最顶部的控制条,这是你的 “驾驶舱”。

1. 核心筛选器 (Filters)

  • application: 对应 JMeter 后端监听器中配置的 application 名。用于切换不同的压测项目。
  • transaction (最关键):
    • 默认是 all:显示所有接口的混合数据。
    • 最佳实践:排查问题时,请务必切换到具体的接口(如 api_order)。因为 “登录” 的快可能会掩盖 “下单” 的慢,混合看数据容易产生误判。
  • Time Range: 右上角的时间选择器。压测时建议选 Last 5 minutes,并开启 Refresh 5s

2. 分区折叠技巧
Grafana 的信息量很大,为了避免干扰,建议先利用行标题左侧的 小箭头 (>) 将所有分区折叠起来,只展开你需要关注的区域。


9.5.2. 局部诊断:Individual Transaction (独立分区)

这是我们最先要关注的地方。请在顶栏 transaction 选择 api_order,然后展开 Individual Transaction - api_order 分区。这里展示了单一接口的健康状况。

image-20251120160119279

1. 核心仪表盘 (Dashboard)

  • Total Requests: 该接口的请求总量。
  • Failed Requests: 该接口的失败数量。
  • Error Rate %: 红线指标。如果这里不是 0,说明该业务功能有 Bug 或服务器已崩溃。

image-20251120160135606

2. 吞吐量趋势 (Throughput)

  • 蓝色面积图:表示每秒处理成功的请求数 (TPS)。
  • 健康形态:应该随着线程数的增加而平滑上升,最后趋于稳定。
  • 病态形态:如果图像出现剧烈的 “断崖式下跌”,说明系统发生了阻塞(如数据库锁死)。

image-20251120160322571

3. 响应时间 (Response Times)
这是判断性能拐点的核心图表。注意图中的几条线:

image-20251120160848727

  • Green (Mean): 平均响应时间。不要只看它,它具有欺骗性,假设马云和 9 个穷人在一起,平均资产是“人人都是亿万富翁”。在性能测试中,如果 99 个请求是 1ms,有 1 个请求卡死用了 10000ms(10 秒),平均时间 大约是 100ms。你看着 100ms 觉得“还行啊”,但那个卡死 10 秒的用户已经卸载你的 App 了。
  • 黄线 (90th Percentile):第 90 名的成绩。

    • 含义:意味着 90% 的用户,响应时间都 快于 这个数值。
  • 蓝线 (95th Percentile):第 95 名的成绩。

    • 含义:意味着 95% 的用户,体验都好于这个数值。这是业界最常用的 SLA (服务等级协议) 标准。如果老板问“我们系统慢不慢”,你就看这条线。
  • 橙线 (99th Percentile):第 99 名的成绩。

    • 含义:这条线代表了 系统的“短板”。如果这条线很高(例如飙升到 3 秒),说明偶尔会有用户遇到严重的卡顿。

Purple (Max): 最大响应时间。如果这条紫线偶尔飙升到几秒,说明存在 “长尾效应”(如 Full GC 停顿)

  • 如果紫线紧贴着橙线(像图中医院):说明系统 极其稳定,没有奇怪的卡顿。
  • 如果紫线偶尔 像针一样刺向天空(例如突然飙到 5000ms),而其他线很低:说明系统有 **“毛刺” **。
  • 常见原因:Java 的垃圾回收 (Full GC) 卡顿、网络抖动、数据库死锁。

9.5.3. 异常分析:Errors (错误分区)

如果 Error Rate 变红了,我们需要立刻展开 Errors 分区来查明原因。

image-20251120161205992

1. 错误分布表 (Errors per Transaction)
左侧表格告诉你 “谁错了”。是所有接口都挂了(可能是网关问题),还是只有 api_order 挂了(代码逻辑问题)。

2. 错误详情表 (Error Info)
右侧表格告诉你 “为什么错”

  • Response Code 500: 服务端内部错误(空指针、数据库连接失败)。
  • Response Code 502/504: 网关超时。说明后端处理太慢,Nginx 等不及了。
    特别注意:JMeter 强制停止导致的误报

如果你在压测过程中点击了 JMeter 的 “STOP” (强制停止) 按钮,Grafana 上可能会突然出现一批错误,报错信息通常为:

  • Non HTTP response code: java.net.SocketException
  • Socket closed

原因:JMeter 暴力断开了连接,导致 InfluxDB 记录了网络异常。
判断方法:如果这些错误仅出现在 压测结束的那一秒,请直接忽略它们,这属于 “人工误报”。


9.5.4. 全局总结:Summary (总结分区)

最后,我们展开最上方的 Summary 分区。这是给老板看 “最终成绩单” 的地方。

1. 宏观计数器

  • Total Requests: 整个压测期间的总发包量。
  • Error Rate %: 全局错误率。互联网应用通常要求小于 0.01%

2. 网络流量 (Received/Sent Bytes)

  • 作用:判断带宽瓶颈。
  • 分析:如果你的 TPS 上不去,CPU 也很闲,但这里显示 Sent/Received 达到了几十 MB/s(接近千兆网卡极限),说明 带宽被打满了。这是很多新手容易忽略的硬件瓶颈。

image-20251120161321182

3. 活跃线程数 (Active Threads)

  • 右下角的紫色柱状图。它展示了并发用户的爬升过程(Ramp-up)。
  • 如果线程数突然 “腰斩”,说明 JMeter 客户端可能因为内存溢出(OOM)而崩溃了。

9.6 本章小结

本章我们搭建了专业的 JIG 监控链路,并学会了如何解读 Grafana 大屏。

核心要点

  1. 架构优势:JMeter + InfluxDB + Grafana 彻底解决了 GUI 消耗资源大、无法回溯历史数据的问题。
  2. 看图逻辑:遵循 “筛选接口 -> 局部诊断 -> 查错 -> 全局总结” 的顺序,避免被海量数据淹没。
  3. 误报识别:压测结束时的 Socket closed 错误通常是强制停止导致的,可忽略。
  4. 瓶颈判断:结合 TPS 曲线、P99 响应时间和网络带宽,综合定位是软件问题还是硬件限制。

第十章. 自动化压测:从命令行到代码化

摘要:在之前的九章中,我们依赖 GUI 界面完成了所有的学习与调试。但在真正的生产级实战中,GUI 是性能的杀手,XML 脚本是维护的噩梦。作为全系列的最终章,我们将跨越 “点点点” 的初级阶段,掌握 CLI 命令行压测 的标准姿势,并引入 JMeter-DSL,让作为 Spring Boot 开发者的你,用最熟悉的 Java 代码来定义压测逻辑,实现真正的工程化交付。

本章学习路径

  • 10.1 摆脱 GUI 束缚:专家级 CLI 压测
    • 10.1.1 “观测者效应”:为什么必须抛弃 GUI?
    • 10.1.2 命令行解剖学:五大核心参数详解
    • 10.1.3 成果验收:原生 HTML 报告解读
  • 10.2 降维打击:JMeter as Code (DSL)
    • 10.2.1 XML 的痛点与 DSL 的崛起
    • 10.2.2 实战:用 Java 重写压测逻辑
    • 10.2.3 运行与集成:像单元测试一样跑压测

10.1. 摆脱 GUI 束缚:专家级 CLI 压测

在之前的章节中,我们一直沉浸在 JMeter 舒适的图形界面(GUI)里。但在真正的企业级生产环境中,GUI 模式是绝对的禁区。本节我们将完成从 “玩具” 到 “工具” 的质变。

10.1.1. 为什么必须抛弃 GUI?(原理层)

在性能测试领域,存在一个著名的 “观测者效应”:当你观察系统时,你的观察行为本身会干扰系统。

对于 JMeter 而言,GUI 模式就是那个干扰源:

  • 资源抢占:JMeter 的图形界面(Swing)需要消耗大量的 CPU 来绘制实时图表,同时消耗大量内存(Heap)来存储临时数据。
  • 性能瓶颈:当并发数超过 500 时,往往服务器还没挂,JMeter 客户端先卡死了。
  • 环境限制:Linux 服务器通常没有显示器,根本无法启动 GUI。

结论:GUI 只用于 编写和调试脚本;正式压测必须使用 CLI (Command Line Interface) 模式。

10.1.2. 命令行解剖学:五大核心参数

在终端中驱动 JMeter,你需要熟练组合以下五个参数。

标准命令模板

1
jmeter -n -t [脚本文件.jmx] -l [结果文件.jtl] -e -o [报告目录]

参数详解:

  1. -n (Non-GUI)

    • 含义:核心开关。明确告诉 JMeter 不要启动图形界面。
    • 后果:如果不加此参数,在无界面的服务器上会直接报错退出。
  2. -t (Test Plan)

    • 含义:指定 “作战蓝图”,即你在 GUI 中保存的 .jmx 脚本文件路径。
    • 注意:路径中严禁包含空格,否则可能识别失败。
  3. -l (Log/Result File)

    • 含义:指定数据存储位置。JMeter 会将每一次请求的详细数据写入这个文件。
    • 铁律该文件必须不存在! 如果文件已存在,JMeter 默认会停止运行(防止覆盖历史数据)。
  4. -e (Export to Dashboard)

    • 含义:压测结束后,自动触发 HTML 报告生成器。
  5. -o (Output Folder)

    • 含义:指定 HTML 报告的产出目录。
    • 铁律该目录必须为空!

10.1.3. 成果验收:原生 HTML 报告解读

执行命令后,进入输出目录双击 index.html,你会看到 JMeter 原生的 Dashboard Report。这里有两个核心指标必须读懂:

1. APDEX (应用性能指数)
在 Dashboard 左上角的仪表盘,这是一个国际通用的用户满意度评分(0.0 ~ 1.0)。

  • > 0.94 (Excellent):用户非常满意。
  • < 0.50 (Unacceptable):用户无法忍受,系统不可用。

2. Statistics (统计摘要表)
在页面下方的表格中,关注以下列:

  • Error %:错误率(底线指标)。
  • 99th pct (P99)核心指标。例如 P99 = 2000ms,意味着 99% 的用户都在 2 秒内得到了响应,只有 1% 的长尾用户遭遇了慢请求。

10.2. 降维打击:JMeter as Code (DSL)

在上一节中,我们学会了用命令行执行 .jmx 脚本。但维护那个几千行的 XML 文件简直是噩梦。本节我们将引入 JMeter-Java-DSL,让作为 Spring Boot 开发者的你,用最熟悉的 Java 代码 来编写压测脚本。

10.2.1. XML 的痛点与 DSL 的崛起

JMeter 原生的 .jmx 本质上是 XML。虽然它对机器友好,但对人类极度不友好:

  • 版本控制地狱:Git Diff 无法看懂 XML 的变动。
  • 无法复用:很难把 “登录逻辑” 提取成一个通用方法。

解决方案:JMeter-Java-DSL
这是一个开源库,它封装了 JMeter 的底层 API,允许我们用 流式风格 (Fluent Style) 的 Java 代码来定义测试计划。压测脚本从此变成了项目代码的一部分。

10.2.2. 实战:用 Java 重写压测逻辑

我们需要创建一个 Maven 模块,并引入 jmeter-java-dsl 依赖。

代码实现

我们将之前 “50 并发下单” 的逻辑翻译成 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.demo.jmeterdemo;

import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.time.Duration;
import static us.abstracta.jmeter.javadsl.JmeterDsl.*; // 静态导入核心方法

public class PerformanceTest {

@Test
public void testOrderApi() throws IOException {
// 1. 定义测试计划
testPlan(
// 2. 定义线程组:名称, 线程数, 循环次数
threadGroup("下单压测组", 50, 10,

// 3. 定义 HTTP 请求
httpSampler("API_下单", "http://localhost:8080/api/order/create")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer my_token_123")
// 定义子组件 (Children)
.children(
// 断言:响应必须包含 code
responseAssertion().containsSubstrings("code"),
// 定时器:随机等待 1~2 秒
uniformRandomTimer(Duration.ofMillis(1000), Duration.ofMillis(2000))
)
),

// 4. 产出报告
htmlReporter("target/jmeter-reports")
).run(); // 5. 启动引擎
}
}

代码解析

  • threadGroup:替代了 GUI 的线程组。
  • httpSampler:替代了 HTTP 请求,支持链式调用 .header()
  • .children():体现了 JMeter 的树状结构,将断言和定时器挂载在请求下方。

10.2.3. 运行与集成

如何运行?
这就和运行普通的单元测试一样简单。在 IDE 中点击 Run,或者在命令行执行 mvn test

结果查看:运行结束后,查看项目目录下的 target/jmeter-reports 文件夹,你将看到生成的 index.html 报告和 report.jtl 数据文件。这意味着,你再也不用手动传递 .jmx 文件了,代码即脚本,所见即所得。


10.3. 本章小结与全系列回顾

随着代码的运行和报告的生成,我们的 JMeter 深度之旅也即将画上句号。

10.3.1. 本章核心要点

  1. CLI 是底线:正式压测请务必使用 jmeter -n -t ...,这是保证数据准确性的前提。
  2. DSL 是未来:对于 Java 开发者,使用 JMeter-DSL 能极大提升脚本的可维护性,并让压测无缝融入 Maven/Gradle 工程体系。
  3. 报告三要素:无论是 CLI 还是 DSL,最终交付的一定是标准的 HTML 报告,关注 Error %P99 即可快速判断系统健康度。

10.3.2. 全系列课程总结

我们从第一章的 Spring Boot 环境搭建 开始,一路解锁了 JMeter 的核心技能树:

  • 基础篇:掌握了线程组、取样器、断言的基本用法,打通了测试闭环。
  • 进阶篇:攻克了参数化、关联、逻辑控制等复杂业务场景,学会了模拟真实用户行为。
  • 高阶篇:利用 JSR223 + Groovy 突破了 GUI 的限制,利用 InfluxDB + Grafana 实现了实时监控。
  • 终极篇:通过 CLI 和 DSL,将压测工程化、代码化。

虽然课程体系非常扎实,但如果要说“精通整个 JMeter”,不得不承认我们在以下三个方面仍存在盲区(这也是由我们的决定的,属于合理的教学取舍):

  1. 非 HTTP 协议:JMeter 其实支持 JDBC (直连数据库)、MQTT (物联网)、WebSocket、TCP 等协议。我们的课程 100% 聚焦于 HTTP/REST API。如果读者遇到 WebSocket 聊天室压测,他们需要通过查阅文档迁移知识。
  2. 分布式集群部署:虽然我们讲了 CLI,但真正的万级并发需要配置 Master-Slave 分布式集群(涉及 RMI 通信、防火墙配置等)。这部分运维属性较重,课程中并未涉及深层配置。
  3. 系统调优:我们教了“如何发现慢”,但没教“如何解决慢”。比如发现 Full GC 了该怎么调 JVM 参数,发现死锁了怎么改代码。这属于架构师领域,超出了 JMeter 工具本身的范畴。

最后的建议
工具只是手段,发现瓶颈 才是目的。JMeter 是一把锋利的剑,但能不能斩断性能问题的荆棘,取决于你对业务的理解和对系统的洞察。

愿你的系统永远 高可用,愿你的 P99 永远 低延迟。同学们,下课!