文章导读:本文将带领读者从零开始,在一台 Ubuntu 26.04 Server 服务器上完成 JMeter 自动化测试服务平台的完整部署
。文章内容涵盖系统安装与初始化、Java 环境搭建、JMeter 安装与配置、平台后端与前端代码实现、服务部署与运行、以及一个完整的实战演示。通过本文,读者可以构建一个基于 Web 的 JMeter 测试平台,实现测试计划的创建、执行、结果收集和报告展示,适用于接口性能测试、压力测试、稳定性测试等多种场景。



一、前言:为什么要搭建 JMeter 自动化测试服务平台

在软件测试领域,性能测试是保障系统稳定性的关键环节。Apache JMeter 作为一款开源的性能测试工具,凭借其强大的协议支持、灵活的测试计划设计、丰富的插件生态以及活跃的社区,已经成为业界使用最广泛的性能测试工具之一。然而,在实际工作中,我们常常会遇到一些场景:测试团队需要多人协作管理测试用例、需要定时执行回归测试、需要将测试结果可视化展示、需要将测试能力集成到 DevOps 流水线中。

传统的 JMeter 使用方式通常是:测试工程师在本地 Windows 或 macOS 上打开 JMeter GUI,手动编写测试计划(.jmx 文件),然后本地执行或传到服务器上执行。这种方式存在以下几个明显的问题:

因此,搭建一个基于服务器的 JMeter 自动化测试服务平台具有重要的现实意义。通过 Web 化的平台,我们可以实现:

本文假设读者具备一定的 Linux 基础知识、Java 编程基础以及对 JMeter 的基本了解。我们将使用 Ubuntu 26.04 Server 作为底层操作系统,OpenJDK 17 作为 Java 运行环境,Apache JMeter 5.6.3 作为测试引擎,Spring Boot 3.2 作为后端服务框架,Vue 3 作为前端框架,MySQL 8.0 作为数据持久化存储,Nginx 作为反向代理。最终目标是构建一个可以运行在生产环境中的自动化测试服务平台。

阅读建议:本文涉及大量命令和代码,建议读者按照章节顺序逐步操作,并在自己的测试服务器或虚拟机上进行实践。所有命令和代码均经过验证,读者可以根据实际环境调整 IP 地址、端口号、用户名等参数。




二、Ubuntu 26.04 Server 系统安装与基础配置

Ubuntu Server 是构建服务器端应用的首选 Linux 发行版之一,它以稳定、安全、易维护著称。Ubuntu 26.04 Server 是 Canonical 公司发布的长期支持(LTS)版本,提供了五年的安全更新和技术支持,非常适合部署企业级测试平台。本节将详细介绍从下载镜像到完成系统基础配置的完整过程。

2.1 下载 Ubuntu 26.04 Server 镜像

首先,我们需要从 Ubuntu 官方网站下载 Ubuntu 26.04 Server 的 ISO 镜像文件。

Ubuntu 官方下载页面(https://ubuntu.com/download/server),选择 Ubuntu 26.04 LTS Server 版本,下载对应的 ISO 文件。通常文件名类似于 ubuntu-26.04-live-server-amd64.iso。建议下载前核对文件的 SHA256 校验值,确保文件在下载过程中没有被损坏或篡改。

如果网络有限制,可以公众号后台回复“ubuntu”获取网盘地址

也可以在命令行中使用 wget 直接下载镜像(以下为示例命令,具体 URL 请以官网实际地址为准):

wget https://releases.ubuntu.com/26.04/ubuntu-26.04-live-server-amd64.iso
sha256sum ubuntu-26.04-live-server-amd64.iso

如果你是在本地虚拟机(如 VMware Workstation、VirtualBox)中安装,可以直接挂载 ISO 文件;如果是在物理服务器上安装,需要制作启动 U 盘。制作启动盘的工具推荐使用 Rufus(Windows)或 balenaEtcher(跨平台)。

2.2 安装 Ubuntu 26.04 Server

将 ISO 镜像挂载到服务器或虚拟机后,启动系统进入安装界面。Ubuntu 26.04 Server 采用基于文本的安装向导,整个安装过程清晰明了。以下是关键步骤的详细说明:

1选择语言:安装向导启动后,首先选择安装语言。建议选择 English,因为在服务器环境中使用英文可以减少因本地化带来的路径或编码问题。后续进入系统后也可以安装中文语言包。

2键盘布局:选择适合的键盘布局,通常默认的 English (US) 即可。如果你使用的是中文键盘,可以选择 Chinese。

3选择安装类型:Ubuntu 26.04 Server 提供多种安装选项。对于我们的测试平台,建议选择 Ubuntu Server(标准服务器安装),不需要选择 Ubuntu Server (minimized) 版本,因为标准版本包含更多常用工具,可以减少后续手动安装的步骤。

4网络配置:安装程序会自动检测网卡并尝试通过 DHCP 获取 IP 地址。如果你的网络环境支持 DHCP,这一步可以直接保持默认。如果服务器需要静态 IP,可以在这一步手动配置。建议为测试服务器配置固定 IP,便于后续通过 SSH 管理和通过浏览器访问平台。

5代理配置:如果服务器需要通过代理才能访问互联网,可以在这里配置代理地址。大多数情况下可以直接留空跳过。

6镜像源选择:安装程序会列出多个 Ubuntu 软件镜像源。对于中国用户,建议选择阿里云、清华大学或中科大镜像源,可以大幅提升软件下载速度。例如:http://mirrors.aliyun.com/ubuntu。

7磁盘分区:根据服务器磁盘大小选择合适的分区方案。对于测试环境,可以选择 Use an entire disk(使用整块磁盘),并启用 LVM(逻辑卷管理),这样后续扩展磁盘更加方便。如果你有特殊的分区需求,也可以选择手动分区。

8创建用户:设置服务器主机名(例如 jmeter-test-platform)、创建一个普通用户(例如 jmeter)并设置密码。注意,Ubuntu Server 默认禁用 root 用户直接登录,因此创建的第一个用户将拥有 sudo 权限。这个用户也将用于后续部署和管理。

9SSH 服务:建议勾选 Install OpenSSH server,这样安装完成后可以通过 SSH 远程管理服务器。同时可以导入 SSH 公钥,实现免密登录。

10特色软件包:安装向导会列出一些可选软件包,如 Docker、PostgreSQL 等。这一步不需要选择任何额外软件包,我们将在后续章节中手动安装所需组件,以便精确控制版本。

完成以上步骤后,安装程序会自动完成系统安装并重启。重启后,使用之前创建的用户名和密码登录系统。如果之前配置了 SSH 公钥,也可以直接通过 SSH 免密登录。

2.3 系统初始化配置

系统安装完成后,需要进行一系列初始化配置,确保服务器安全、稳定、便于维护。以下是建议的初始化步骤:

2.3.1 更新系统软件包

首先,使用以下命令更新系统的软件包索引和已安装软件包。这一步可以修复已知的安全漏洞,并确保系统中的工具处于最新状态。

sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y

其中,apt update 用于更新软件包列表,apt upgrade -y 用于升级所有可升级的软件包,apt autoremove -y 用于清理不再需要的依赖包。执行这些命令可能需要几分钟时间,具体取决于网络速度和系统状态。

2.3.2 配置静态 IP 地址

如果服务器在部署时没有配置静态 IP,建议通过 Netplan 配置固定 IP。Ubuntu 22.04 及以后版本使用 Netplan 管理网络配置。编辑 Netplan 配置文件:

sudo nano /etc/netplan/00-installer-config.yaml

将文件内容修改为如下示例(请根据实际网络环境调整 IP、网关和 DNS):

network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      addresses:
        - 192.168.1.100/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses:
          - 114.114.114.114
          - 8.8.8.8

保存文件后,应用网络配置:

sudo netplan apply

可以通过 ip addr 命令验证 IP 地址是否生效。

2.3.3 配置 SSH 安全登录

为了提高安全性,建议对 SSH 服务进行基础加固。编辑 SSH 配置文件:

sudo nano /etc/ssh/sshd_config

建议进行以下修改:

修改完成后,重启 SSH 服务:

sudo systemctl restart sshd

注意事项:修改 SSH 配置时,建议保留一个已经登录的会话窗口,以防配置错误导致无法连接。修改端口后,登录命令需要加上 -p 参数,例如 ssh -p 2222 jmeter@192.168.1.100。

2.3.4 安装常用工具

在后续部署过程中,我们需要使用一些常用的命令行工具。建议提前安装:

sudo apt install -y vim wget curl unzip net-tools htop git build-essential

各工具用途说明:

2.3.5 配置时区和时间同步

测试平台中的日志、报告和调度任务都依赖准确的时间。建议将服务器时区设置为亚洲/上海,并启用时间同步服务:

sudo timedatectl set-timezone Asia/Shanghai
sudo timedatectl set-ntp true
timedatectl status

2.3.6 创建专用运行用户

为了安全起见,建议为测试平台创建一个专用的运行用户,避免使用 root 用户部署应用。这个用户只拥有必要的权限,降低潜在风险。

sudo useradd -m -s /bin/bash jmeter-app
sudo passwd jmeter-app
sudo usermod -aG sudo jmeter-app

执行 passwd 命令后,按提示设置用户密码。后续部署过程中,我们将主要使用 jmeter-app 用户操作。

2.4 安装与配置防火墙

服务器安全是部署过程中不可忽视的环节。Ubuntu 默认使用 ufw(Uncomplicated Firewall)作为防火墙管理工具。我们需要配置防火墙,仅开放必要的端口。

首先,启用 ufw 并设置默认策略:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8080/tcp
sudo ufw enable

这里开放的端口说明:

查看防火墙状态:

sudo ufw status verbose

完成以上基础配置后,服务器已经具备部署 JMeter 测试平台的基本环境。下一节将介绍 Java 运行环境的安装与配置。



三、Java 环境安装与变量配置

Apache JMeter 是基于 Java 开发的桌面应用程序,因此运行 JMeter 必须依赖 Java 运行环境(JRE)或 Java 开发工具包(JDK)。同时,我们的后端服务采用 Spring Boot 框架开发,也需要 JDK 来编译和运行。因此,安装和配置 Java 环境是部署测试平台的关键步骤之一。

3.1 Java 版本选择

Apache JMeter 5.6.x 官方支持 Java 8、Java 11、Java 17 和 Java 21。为了保证兼容性和性能,推荐使用 Java 17 或 Java 21。本文选择 OpenJDK 17 作为基础 Java 环境,因为 Java 17 是长期支持(LTS)版本,既稳定又拥有较好的性能表现,且与 Spring Boot 3.x 完美兼容。

在 Ubuntu 系统中,可以通过两种方式安装 Java:

本文采用 apt 方式安装 OpenJDK 17,这种方式最稳定,也最容易维护。

3.2 安装 OpenJDK 17

首先,更新软件包列表,然后安装 OpenJDK 17:

sudo apt update
sudo apt install -y openjdk-17-jdk

安装过程可能需要几分钟,具体取决于网络速度。安装完成后,可以通过以下命令验证 Java 是否安装成功:

java -version
javac -version

正常输出应类似如下内容:

openjdk version "17.0.8" 2023-07-18
OpenJDK Runtime Environment (build 17.0.8+7-Ubuntu-122.04.2)
OpenJDK 64-Bit Server VM (build 17.0.8+7-Ubuntu-122.04.2, mixed mode, sharing)

javac 17.0.8

如果系统中同时安装了多个 Java 版本,可以使用 update-alternatives 命令切换默认的 Java 版本:

sudo update-alternatives --config java

在弹出的列表中选择 Java 17 对应的编号即可。建议将 Java 17 设置为默认版本。

3.3 配置 JAVA_HOME 环境变量

很多 Java 应用(包括 JMeter 和 Maven)在运行时需要知道 JDK 的安装路径,也就是 JAVA_HOME 环境变量。正确配置 JAVA_HOME 可以避免后续出现各种找不到 Java 或路径错误的问题。

首先,找到 OpenJDK 17 的安装路径:

readlink -f $(which java)

输出通常类似:

/usr/lib/jvm/java-17-openjdk-amd64/bin/java

因此,JAVA_HOME 应设置为 /usr/lib/jvm/java-17-openjdk-amd64(去掉末尾的 /bin/java)。

接下来,编辑全局环境变量配置文件:

sudo nano /etc/profile.d/jdk.sh

在文件中添加以下内容:

export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib

保存并退出。然后使环境变量生效:

source /etc/profile.d/jdk.sh

验证 JAVA_HOME 是否配置成功:

echo $JAVA_HOME
$JAVA_HOME/bin/java -version

如果输出正确的路径和 Java 版本信息,说明配置成功。

3.4 安装 Maven 构建工具

由于后端服务使用 Spring Boot 开发,需要通过 Maven 进行项目构建。Maven 是 Java 项目最常用的构建工具之一,能够管理依赖、编译代码、打包应用。

通过 apt 安装 Maven:

sudo apt install -y maven

验证安装:

mvn -version

输出示例:

Apache Maven 3.8.7
Maven home: /usr/share/maven
Java version: 17.0.8, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64

如果需要使用特定版本的 Maven,也可以从 Apache 官网下载二进制包并手动配置环境变量。不过对于我们的项目,apt 安装的版本已经足够。

3.5 Java 环境配置检查清单

完成以上步骤后,建议再次确认以下环境变量和命令是否正常:

检查项
命令
预期结果
Java 版本
java -version
显示 OpenJDK 17
Java 编译器
javac -version
显示 17.x
JAVA_HOME
echo $JAVA_HOME
/usr/lib/jvm/java-17-openjdk-amd64
Maven 版本
mvn -version
显示 Apache Maven 3.x
Java 可执行路径
which java
指向 JAVA_HOME/bin/java

如果所有检查项都通过,说明 Java 环境已经准备就绪。下一节将安装 Apache JMeter 并进行配置。



四、JMeter 安装、配置与插件扩展

Apache JMeter 是一个纯 Java 编写的桌面应用程序,用于性能测试和功能测试。它可以模拟大量用户对服务器、网络或对象进行压力测试,支持 HTTP/HTTPS、FTP、JDBC、JMS、SOAP、REST 等多种协议。本节将介绍如何在 Ubuntu 26.04 Server 上安装和配置 JMeter,并安装常用插件扩展其功能。

4.1 JMeter 版本选择

本文选择 Apache JMeter 5.6.3 作为测试引擎。这是目前较为稳定的版本,支持 Java 17,具有丰富的插件生态,并且对现代 Web 应用测试有良好的支持。建议从 JMeter 官方网站下载二进制发布包,避免使用第三方修改版。

访问 Apache JMeter 下载页面:https://jmeter.apache.org/download_jmeter.cgi,找到 Binaries 部分的 apache-jmeter-5.6.3.zip 或 apache-jmeter-5.6.3.tgz 下载链接。

4.2 下载与解压 JMeter

以 jmeter-app 用户登录服务器,或使用普通用户执行以下命令。我们将 JMeter 安装到 /opt/jmeter 目录,这是一个常用的系统级软件安装目录。

cd /tmp
wget https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.3.zip

# 如果官方镜像下载较慢,可以替换为国内镜像或 GitHub 加速地址
# 例如使用清华镜像:
# wget https://mirrors.tuna.tsinghua.edu.cn/apache/jmeter/binaries/apache-jmeter-5.6.3.zip

下载完成后,解压并移动到安装目录:

unzip apache-jmeter-5.6.3.zip
sudo mv apache-jmeter-5.6.3 /opt/jmeter
sudo chown -R jmeter-app:jmeter-app /opt/jmeter

这里使用 chown 将目录所有者改为 jmeter-app 用户,确保后续运行测试时该用户对 JMeter 目录有读写权限。

4.3 配置 JMeter 环境变量

为了方便在命令行中直接调用 JMeter,我们需要配置 JMeter 的环境变量。编辑 /etc/profile.d/jmeter.sh 文件:

sudo nano /etc/profile.d/jmeter.sh

添加以下内容:

export JMETER_HOME=/opt/jmeter
export PATH=$JMETER_HOME/bin:$PATH

使环境变量生效:

source /etc/profile.d/jmeter.sh

验证 JMeter 安装是否成功:

jmeter -v

正常输出应包含 JMeter 版本信息,例如:

Copyright (c) 1999-2024 The Apache Software Foundation
Version 5.6.3

4.4 修改 JMeter 内存配置

JMeter 在运行大规模并发测试时需要较大的内存。默认情况下,JMeter 的堆内存设置较小,可能无法满足高并发测试需求。我们需要修改 JMeter 的启动脚本,增大内存分配。

编辑 /opt/jmeter/bin/jmeter 文件(Linux 启动脚本):

sudo nano /opt/jmeter/bin/jmeter

找到以下配置(大约在文件开头):

HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"

根据服务器内存大小进行修改。例如,如果服务器有 8GB 内存,可以设置为:

HEAP="-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m"

其中 -Xms 是初始堆内存,-Xmx 是最大堆内存,-XX:MaxMetaspaceSize 是最大元空间大小。建议根据实际测试需求和服务器的物理内存进行调整,不要将 Xmx 设置过大,否则会导致系统频繁进行垃圾回收或内存交换。

4.5 安装 JMeter 插件管理器

JMeter 插件管理器(JMeter Plugins Manager)是扩展 JMeter 功能的必备工具。通过它,我们可以方便地安装各种图表插件、协议插件、采样器插件等。最常用的插件包括:

安装插件管理器的方法很简单:从 JMeter Plugins 官网下载 jmeter-plugins-manager-1.9.jar,将其放到 JMeter 的 lib/ext 目录下即可。

cd /tmp
wget https://jmeter-plugins.org/get/ -O jmeter-plugins-manager.jar
sudo mv jmeter-plugins-manager.jar /opt/jmeter/lib/ext/
sudo chown jmeter-app:jmeter-app /opt/jmeter/lib/ext/jmeter-plugins-manager.jar

安装完成后,重新启动 JMeter GUI 时,菜单栏会出现 “Plugins” 菜单。由于我们在服务器上运行,通常使用 CLI 模式,因此插件管理器主要用于安装必要的 jar 文件到 lib/ext 目录。安装完成后,可以在 GUI 环境(本地开发机)中选择需要的插件,再将生成的依赖包上传到服务器的 lib/ext 目录。

4.6 验证 JMeter 命令行执行

在服务器环境中,我们通常使用 JMeter 的非 GUI 模式(CLI 模式)执行测试,因为 GUI 模式会消耗大量资源,不适合高并发测试。非 GUI 模式的命令格式如下:

jmeter -n -t /path/to/test-plan.jmx -l /path/to/results.jtl -e -o /path/to/report

参数说明:

为了验证 JMeter 是否正常工作,我们可以创建一个简单的测试计划。首先,在本地 JMeter GUI 中创建一个 HTTP 请求测试计划,保存为 /opt/jmeter/test-samples/demo.jmx。如果暂时没有测试计划,可以使用以下命令检查 JMeter 是否能正常启动:

jmeter -n -t /opt/jmeter/extras/Test.jmx -l /tmp/test.jtl -e -o /tmp/test-report

如果看到类似 “summary = 1 in 00:00:01 = 0.9/s Avg: 5 Min: 5 Max: 5 Err: 0 (0.00%)” 的输出,说明 JMeter 安装成功并能正常执行测试。

提示:非 GUI 模式下,JMeter 无法直接展示 GUI 中的插件图表。因此,如果需要使用 PerfMon 等监控插件,建议通过 JMeter 的后端监听器(Backend Listener)将数据发送到 InfluxDB + Grafana,或者使用 JMeter 生成的 HTML 报告查看结果。

4.7 JMeter 目录结构说明

了解 JMeter 的目录结构有助于后续进行配置和扩展。主要目录说明如下:

目录
说明
bin/
存放 JMeter 启动脚本、配置文件、示例测试计划。
lib/
存放 JMeter 运行所需的核心库和依赖 jar。
lib/ext/
存放 JMeter 插件 jar 文件。
extras/
存放一些额外工具和示例脚本。
printable_docs/
JMeter 官方文档。




五、平台整体架构设计与技术选型

在正式开始编写代码之前,我们需要对整个测试平台的架构有一个清晰的认识。一个良好的架构设计能够指导后续的开发和部署,确保系统具有良好的可扩展性、可维护性和稳定性。

5.1 业务需求分析

我们构建的 JMeter 自动化测试服务平台,核心目标是让测试人员能够方便地通过 Web 界面对测试计划进行管理、执行和结果查看。具体业务需求如下:

5.2 整体架构设计

基于以上需求,平台采用前后端分离的微服务架构。整体架构如下:

微信公众号二维码图片引自微信公众号,扫码关注阅读原文


架构层次说明:

5.3 技术选型

根据架构设计,各层技术选型如下:

层次
技术
版本
选型理由
操作系统
Ubuntu Server
26.04 LTS
稳定、长期支持、社区活跃。
Java 环境
OpenJDK
17
LTS 版本,与 Spring Boot 3.x 和 JMeter 5.6 兼容。
测试引擎
Apache JMeter
5.6.3
功能强大、插件丰富、生态成熟。
后端框架
Spring Boot
3.2.x
开发效率高、生态完善、易于部署。
前端框架
Vue 3 + Element Plus
3.4.x
组件丰富、文档完善、适合管理后台。
数据库
MySQL
8.0
稳定、性能好、适合结构化数据存储。
反向代理
Nginx
1.24
高性能、低资源占用、配置灵活。
进程管理
systemd
–
Ubuntu 默认服务管理工具,方便管理应用生命周期。

5.4 数据模型设计

在数据库设计方面,我们需要至少以下几个核心表:

这些表之间的关系为:一个用户可以创建多个测试计划;一个测试计划可以被多次执行,产生多个测试任务;一个测试任务对应一个测试报告。后续在代码实现部分,我们将详细展示这些表的建表语句和实体类定义。

5.5 安全设计考虑

测试平台虽然主要供内部团队使用,但仍需注意基本的安全设计:




六、后端服务实现:Spring Boot 与 JMeter 集成

后端服务是整个平台的核心,负责业务逻辑处理、文件管理、任务调度和结果解析。本节将详细介绍如何使用 Spring Boot 3.2 构建后端服务,并实现与 JMeter 的集成。

6.1 创建 Spring Boot 项目

我们可以使用 Spring Initializr(https://start.spring.io/)创建项目,也可以直接在服务器上使用命令行创建。本文推荐使用 Spring Initializr 的 Web 界面生成项目基础骨架,然后导入到 IDEA 或 VS Code 中开发。在服务器上,我们可以直接使用 Maven 命令构建。

创建项目时选择以下依赖:

为了简化,本文的示例代码将使用 Spring Boot + Spring Data JPA + MySQL + JWT 的组合。我们首先创建项目目录结构:

mkdir -p ~/projects/jmeter-platform
cd ~/projects/jmeter-platform

在项目目录下创建 pom.xml 文件,配置 Maven 依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>jmeter-platform</artifactId>
    <version>1.0.0</version>
    <name>jmeter-platform</name>
    <description>JMeter Automation Testing Platform</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.3</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.12.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

6.2 项目目录结构

一个清晰的项目目录结构有助于代码维护。建议的目录结构如下:

jmeter-platform/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/jmeterplatform/
│   │   │       ├── JmeterPlatformApplication.java
│   │   │       ├── config/
│   │   │       │   ├── SecurityConfig.java
│   │   │       │   ├── JwtConfig.java
│   │   │       │   └── WebConfig.java
│   │   │       ├── controller/
│   │   │       │   ├── AuthController.java
│   │   │       │   ├── TestPlanController.java
│   │   │       │   ├── TestTaskController.java
│   │   │       │   └── ReportController.java
│   │   │       ├── entity/
│   │   │       │   ├── User.java
│   │   │       │   ├── TestPlan.java
│   │   │       │   ├── TestTask.java
│   │   │       │   └── TestReport.java
│   │   │       ├── repository/
│   │   │       │   ├── UserRepository.java
│   │   │       │   ├── TestPlanRepository.java
│   │   │       │   ├── TestTaskRepository.java
│   │   │       │   └── TestReportRepository.java
│   │   │       ├── service/
│   │   │       │   ├── UserService.java
│   │   │       │   ├── TestPlanService.java
│   │   │       │   ├── TestTaskService.java
│   │   │       │   └── ReportService.java
│   │   │       ├── dto/
│   │   │       │   ├── LoginRequest.java
│   │   │       │   ├── ExecuteTaskRequest.java
│   │   │       │   └── ApiResponse.java
│   │   │       └── util/
│   │   │           ├── JwtUtil.java
│   │   │           └── JmxUtil.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-prod.yml
│   │       └── static/
│   └── test/
│       └── java/
└── uploads/    # 运行时文件上传目录(生产环境可配置为外部目录)

6.3 配置文件

在 src/main/resources/application.yml 中配置数据库连接、文件上传路径、JMeter 路径等:

server:
  port: 8080

spring:
  application:
    name: jmeter-platform
  datasource:
    url: jdbc:mysql://localhost:3306/jmeter_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: jmeter
    password: Jmeter@123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB

jmeter:
  home: /opt/jmeter
  upload-dir: /var/lib/jmeter-platform/uploads
  report-dir: /var/lib/jmeter-platform/reports

jwt:
  secret: your-256-bit-secret-key-for-jwt-signing-must-be-at-least-32-characters
  expiration: 86400000

注意:在生产环境中,密码和 JWT 密钥不应该直接写在配置文件中,应该通过环境变量或外部配置中心注入。这里为了演示方便,直接写入了配置文件中。

6.4 实体类定义

以下是核心实体类的定义。我们使用 JPA 注解进行对象关系映射。

User.java(用户实体):

package com.example.jmeterplatform.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role = "USER";

    @Column(nullable = false)
    private Boolean enabled = true;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

TestPlan.java(测试计划实体):

package com.example.jmeterplatform.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "test_plan")
@Data
public class TestPlan {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private String description;

    @Column(name = "file_path", nullable = false)
    private String filePath;

    @Column(name = "created_by")
    private Long createdBy;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

TestTask.java(测试任务实体):

package com.example.jmeterplatform.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "test_task")
@Data
public class TestTask {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "plan_id", nullable = false)
    private Long planId;

    @Column(nullable = false)
    private String status = "PENDING"; // PENDING, RUNNING, SUCCESS, FAILED, CANCELLED

    @Column(name = "thread_count")
    private Integer threadCount = 10;

    @Column(name = "duration_seconds")
    private Integer durationSeconds = 60;

    @Column(name = "ramp_up_seconds")
    private Integer rampUpSeconds = 10;

    @Column(name = "jtl_path")
    private String jtlPath;

    @Column(name = "report_path")
    private String reportPath;

    @Column(name = "log_output")
    private String logOutput;

    @Column(name = "error_message")
    private String errorMessage;

    @Column(name = "started_at")
    private LocalDateTime startedAt;

    @Column(name = "finished_at")
    private LocalDateTime finishedAt;

    @Column(name = "created_by")
    private Long createdBy;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
    }
}

TestReport.java(测试报告实体):

package com.example.jmeterplatform.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Entity
@Table(name = "test_report")
@Data
public class TestReport {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "task_id", nullable = false)
    private Long taskId;

    @Column(name = "sample_count")
    private Long sampleCount;

    @Column(name = "error_count")
    private Long errorCount;

    @Column(name = "error_rate")
    private Double errorRate;

    @Column(name = "avg_response_time")
    private Double avgResponseTime;

    @Column(name = "min_response_time")
    private Long minResponseTime;

    @Column(name = "max_response_time")
    private Long maxResponseTime;

    @Column(name = "throughput")
    private Double throughput;

    @Column(name = "report_path")
    private String reportPath;

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
    }
}

6.5 JMeter 执行服务

核心功能之一是启动 JMeter 进程执行测试计划。我们使用 Java 的 ProcessBuilder 来启动 JMeter CLI 进程。执行服务需要处理参数构造、进程启动、输出捕获、状态更新和异常处理。

TestTaskService.java 的核心执行方法:

package com.example.jmeterplatform.service;

import com.example.jmeterplatform.dto.ExecuteTaskRequest;
import com.example.jmeterplatform.entity.TestPlan;
import com.example.jmeterplatform.entity.TestReport;
import com.example.jmeterplatform.entity.TestTask;
import com.example.jmeterplatform.repository.TestPlanRepository;
import com.example.jmeterplatform.repository.TestReportRepository;
import com.example.jmeterplatform.repository.TestTaskRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class TestTaskService {

    private final TestTaskRepository taskRepository;
    private final TestPlanRepository planRepository;
    private final TestReportRepository reportRepository;

    @Value("${jmeter.home}")
    private String jmeterHome;

    @Value("${jmeter.report-dir}")
    private String reportDir;

    @Transactional
    public TestTask createTask(Long planId, ExecuteTaskRequest request, Long userId) {
        TestPlan plan = planRepository.findById(planId)
                .orElseThrow(() -> new RuntimeException("测试计划不存在"));

        TestTask task = new TestTask();
        task.setPlanId(planId);
        task.setThreadCount(request.getThreadCount());
        task.setDurationSeconds(request.getDurationSeconds());
        task.setRampUpSeconds(request.getRampUpSeconds());
        task.setStatus("PENDING");
        task.setCreatedBy(userId);
        taskRepository.save(task);
        return task;
    }

    @Async("taskExecutor")
    public void executeTask(Long taskId) {
        TestTask task = taskRepository.findById(taskId)
                .orElseThrow(() -> new RuntimeException("任务不存在"));

        TestPlan plan = planRepository.findById(task.getPlanId())
                .orElseThrow(() -> new RuntimeException("测试计划不存在"));

        task.setStatus("RUNNING");
        task.setStartedAt(LocalDateTime.now());
        taskRepository.save(task);

        String taskUuid = UUID.randomUUID().toString();
        Path taskReportDir = Paths.get(reportDir, taskUuid);
        Path jtlFile = Paths.get(taskReportDir.toString(), "result.jtl");
        Path reportOutputDir = Paths.get(taskReportDir.toString(), "report");

        try {
            Files.createDirectories(taskReportDir);
            Files.createDirectories(reportOutputDir);

            String jmeterPath = Paths.get(jmeterHome, "bin", "jmeter").toString();
            String jmxPath = plan.getFilePath();

            ProcessBuilder pb = new ProcessBuilder(
                    jmeterPath,
                    "-n",
                    "-t", jmxPath,
                    "-l", jtlFile.toString(),
                    "-e",
                    "-o", reportOutputDir.toString(),
                    "-JthreadCount=" + task.getThreadCount(),
                    "-JdurationSeconds=" + task.getDurationSeconds(),
                    "-JrampUpSeconds=" + task.getRampUpSeconds()
            );

            pb.redirectErrorStream(true);
            pb.environment().put("JAVA_HOME", System.getenv("JAVA_HOME"));
            pb.environment().put("JMETER_HOME", jmeterHome);

            Process process = pb.start();

            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                    log.info("[JMeter-{}] {}", taskId, line);
                }
            }

            int exitCode = process.waitFor();
            task.setLogOutput(output.toString());
            task.setJtlPath(jtlFile.toString());
            task.setReportPath(reportOutputDir.toString());
            task.setFinishedAt(LocalDateTime.now());

            if (exitCode == 0) {
                task.setStatus("SUCCESS");
                generateReportSummary(task, jtlFile.toString(), reportOutputDir.toString());
            } else {
                task.setStatus("FAILED");
                task.setErrorMessage("JMeter 进程异常退出,退出码:" + exitCode);
            }
        } catch (Exception e) {
            log.error("执行任务失败: {}", taskId, e);
            task.setStatus("FAILED");
            task.setErrorMessage(e.getMessage());
            task.setFinishedAt(LocalDateTime.now());
        } finally {
            taskRepository.save(task);
        }
    }

    private void generateReportSummary(TestTask task, String jtlPath, String reportPath) {
        // 这里可以扩展为解析 JTL 文件并生成统计指标
        // 简化示例:只保存报告路径
        TestReport report = new TestReport();
        report.setTaskId(task.getId());
        report.setReportPath(reportPath);
        reportRepository.save(report);
    }
}

关键设计说明:这里使用 @Async("taskExecutor") 将 JMeter 执行放在异步线程池中,避免 HTTP 请求长时间阻塞。在 Spring Boot 中需要配置一个异步任务执行器,并通过 @EnableAsync 启用异步支持。JMeter 参数通过 -J 选项传递,这些参数可以在 JMX 文件中使用 ${__P(threadCount)} 等函数引用,实现动态控制并发数、持续时间等。

6.6 异步执行器配置

在主类或配置类中启用异步支持并配置线程池:

package com.example.jmeterplatform.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("jmeter-task-");
        executor.initialize();
        return executor;
    }
}

6.7 控制器层

控制器负责接收前端请求并调用业务服务。以下是测试计划控制器和任务控制器的示例。

TestPlanController.java:

package com.example.jmeterplatform.controller;

import com.example.jmeterplatform.dto.ApiResponse;
import com.example.jmeterplatform.entity.TestPlan;
import com.example.jmeterplatform.service.TestPlanService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequestMapping("/api/plans")
@RequiredArgsConstructor
public class TestPlanController {

    private final TestPlanService planService;

    @GetMapping
    public ResponseEntity<ApiResponse<List<TestPlan>>> listPlans() {
        return ResponseEntity.ok(ApiResponse.success(planService.listAll()));
    }

    @PostMapping
    public ResponseEntity<ApiResponse<TestPlan>> uploadPlan(
            @RequestParam("name") String name,
            @RequestParam("description") String description,
            @RequestParam("file") MultipartFile file) {
        return ResponseEntity.ok(ApiResponse.success(planService.savePlan(name, description, file)));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<TestPlan>> getPlan(@PathVariable Long id) {
        return ResponseEntity.ok(ApiResponse.success(planService.getById(id)));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deletePlan(@PathVariable Long id) {
        planService.deletePlan(id);
        return ResponseEntity.ok(ApiResponse.success(null));
    }
}

TestTaskController.java:

package com.example.jmeterplatform.controller;

import com.example.jmeterplatform.dto.ApiResponse;
import com.example.jmeterplatform.dto.ExecuteTaskRequest;
import com.example.jmeterplatform.entity.TestTask;
import com.example.jmeterplatform.service.TestTaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TestTaskController {

    private final TestTaskService taskService;

    @PostMapping("/execute/{planId}")
    public ResponseEntity<ApiResponse<TestTask>> execute(@PathVariable Long planId,
                                                          @RequestBody ExecuteTaskRequest request) {
        // 实际项目中应从 SecurityContext 获取当前用户 ID
        Long userId = 1L;
        TestTask task = taskService.createTask(planId, request, userId);
        taskService.executeTask(task.getId());
        return ResponseEntity.ok(ApiResponse.success(task));
    }

    @GetMapping
    public ResponseEntity<ApiResponse<List<TestTask>>> listTasks() {
        return ResponseEntity.ok(ApiResponse.success(taskService.listAll()));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<TestTask>> getTask(@PathVariable Long id) {
        return ResponseEntity.ok(ApiResponse.success(taskService.getById(id)));
    }

    @GetMapping("/{id}/log")
    public ResponseEntity<ApiResponse<String>> getTaskLog(@PathVariable Long id) {
        return ResponseEntity.ok(ApiResponse.success(taskService.getTaskLog(id)));
    }
}

由于篇幅限制,User、TestPlanService、Repository 接口、DTO 等代码的完整实现可以参考项目源码。在下一节中,我们将介绍前端 Vue 3 的实现。



七、前端界面实现:Vue 3 测试控制台

前端界面是用户与测试平台交互的窗口。一个设计良好的前端界面能够显著提升测试人员的工作效率。本节将介绍如何使用 Vue 3 + Element Plus 构建一个简洁、实用的测试控制台。

7.1 前端项目初始化

我们将使用 Vue CLI 创建前端项目。如果你的开发环境中还没有安装 Vue CLI,可以先通过 npm 安装:

npm install -g @vue/cli
# 或者使用 create-vue(Vue 官方推荐)
npm create vue@latest jmeter-platform-ui

本文使用 create-vue 创建项目,选择 Vue 3 + TypeScript + Vite + Element Plus。创建完成后,进入项目目录并安装依赖:

cd jmeter-platform-ui
npm install
npm install element-plus axios vue-router pinia

依赖说明:

7.2 项目目录结构

建议的前端项目目录结构如下:

jmeter-platform-ui/
├── public/
├── src/
│   ├── api/
│   │   ├── plan.ts
│   │   ├── task.ts
│   │   └── auth.ts
│   ├── assets/
│   ├── components/
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   └── user.ts
│   ├── views/
│   │   ├── LoginView.vue
│   │   ├── PlanListView.vue
│   │   ├── PlanUploadView.vue
│   │   ├── TaskListView.vue
│   │   └── TaskDetailView.vue
│   ├── App.vue
│   └── main.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

7.3 API 封装

在前端项目中,我们通常将 API 调用封装到独立的模块中,便于统一管理和复用。以下是 src/api/plan.ts 的示例:

import axios from 'axios';

const API_BASE = '/api';

export interface TestPlan {
    id: number;
    name: string;
    description: string;
    filePath: string;
    createdAt: string;
    updatedAt: string;
}

export interface ApiResponse<T> {
    code: number;
    message: string;
    data: T;
}

export const planApi = {
    list: () => axios.get<ApiResponse<TestPlan[]>>(`${API_BASE}/plans`),
    get: (id: number) => axios.get<ApiResponse<TestPlan>>(`${API_BASE}/plans/${id}`),
    upload: (formData: FormData) => axios.post<ApiResponse<TestPlan>>(
        `${API_BASE}/plans`, formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
        }),
    delete: (id: number) => axios.delete<ApiResponse<void>>(`${API_BASE}/plans/${id}`)
};

src/api/task.ts 的示例:

import axios from 'axios';

const API_BASE = '/api';

export interface TestTask {
    id: number;
    planId: number;
    status: string;
    threadCount: number;
    durationSeconds: number;
    rampUpSeconds: number;
    jtlPath: string;
    reportPath: string;
    logOutput: string;
    errorMessage: string;
    startedAt: string;
    finishedAt: string;
}

export const taskApi = {
    list: () => axios.get<ApiResponse<TestTask[]>>(`${API_BASE}/tasks`),
    get: (id: number) => axios.get<ApiResponse<TestTask>>(`${API_BASE}/tasks/${id}`),
    getLog: (id: number) => axios.get<ApiResponse<string>>(`${API_BASE}/tasks/${id}/log`),
    execute: (planId: number, params: object) => axios.post<ApiResponse<TestTask>>(
        `${API_BASE}/tasks/execute/${planId}`, params)
};

7.4 测试计划列表页面

PlanListView.vue 用于展示所有已上传的测试计划,并提供上传、执行和删除操作:

<template>
  <div class="plan-list">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>测试计划管理</span>
          <el-button type="primary" @click="showUploadDialog = true">上传计划</el-button>
        </div>
      </template>

      <el-table :data="plans" v-loading="loading">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="name" label="计划名称" />
        <el-table-column prop="description" label="描述" />
        <el-table-column prop="createdAt" label="创建时间" />
        <el-table-column label="操作" width="240">
          <template #default="scope">
            <el-button size="small" @click="executePlan(scope.row)">执行</el-button>
            <el-button size="small" type="danger" @click="deletePlan(scope.row.id)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <el-dialog v-model="showUploadDialog" title="上传测试计划" width="500px">
      <el-form :model="uploadForm" label-width="80px">
        <el-form-item label="计划名称">
          <el-input v-model="uploadForm.name" />
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="uploadForm.description" type="textarea" />
        </el-form-item>
        <el-form-item label="JMX 文件">
          <el-upload ref="uploadRef" :auto-upload="false" :on-change="handleFileChange">
            <el-button>选择文件</el-button>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showUploadDialog = false">取消</el-button>
        <el-button type="primary" @click="submitUpload">上传</el-button>
      </template>
    </el-dialog>

    <el-dialog v-model="showExecuteDialog" title="执行测试" width="500px">
      <el-form :model="executeForm" label-width="120px">
        <el-form-item label="并发线程数">
          <el-input-number v-model="executeForm.threadCount" :min="1" :max="1000" />
        </el-form-item>
        <el-form-item label="持续时间(秒)">
          <el-input-number v-model="executeForm.durationSeconds" :min="1" :max="3600" />
        </el-form-item>
        <el-form-item label="启动时间(秒)">
          <el-input-number v-model="executeForm.rampUpSeconds" :min="1" :max="300" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showExecuteDialog = false">取消</el-button>
        <el-button type="primary" @click="submitExecute">启动</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { planApi, type TestPlan } from '@/api/plan';
import { taskApi } from '@/api/task';

const plans = ref<TestPlan[]>([]);
const loading = ref(false);
const showUploadDialog = ref(false);
const showExecuteDialog = ref(false);
const selectedPlan = ref<TestPlan | null>(null);
const uploadFile = ref<File | null>(null);

const uploadForm = ref({ name: '', description: '' });
const executeForm = ref({ threadCount: 10, durationSeconds: 60, rampUpSeconds: 10 });

const loadPlans = async () => {
  loading.value = true;
  try {
    const res = await planApi.list();
    plans.value = res.data.data;
  } catch (error) {
    ElMessage.error('加载测试计划失败');
  } finally {
    loading.value = false;
  }
};

const handleFileChange = (file: any) => {
  uploadFile.value = file.raw;
};

const submitUpload = async () => {
  if (!uploadFile.value) {
    ElMessage.warning('请选择 JMX 文件');
    return;
  }
  const formData = new FormData();
  formData.append('name', uploadForm.value.name);
  formData.append('description', uploadForm.value.description);
  formData.append('file', uploadFile.value);

  try {
    await planApi.upload(formData);
    ElMessage.success('上传成功');
    showUploadDialog.value = false;
    loadPlans();
  } catch (error) {
    ElMessage.error('上传失败');
  }
};

const executePlan = (plan: TestPlan) => {
  selectedPlan.value = plan;
  showExecuteDialog.value = true;
};

const submitExecute = async () => {
  if (!selectedPlan.value) return;
  try {
    await taskApi.execute(selectedPlan.value.id, executeForm.value);
    ElMessage.success('测试任务已启动');
    showExecuteDialog.value = false;
  } catch (error) {
    ElMessage.error('启动失败');
  }
};

const deletePlan = async (id: number) => {
  try {
    await ElMessageBox.confirm('确定删除该测试计划吗?', '提示', { type: 'warning' });
    await planApi.delete(id);
    ElMessage.success('删除成功');
    loadPlans();
  } catch (error) {
    // 用户取消或删除失败
  }
};

onMounted(() => {
  loadPlans();
});
</script>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.plan-list {
  padding: 20px;
}
</style>
微信公众号二维码图片引自微信公众号,扫码关注阅读原文

7.5 任务列表与详情页面

任务列表页面用于展示所有测试任务的执行状态,任务详情页面用于查看任务日志和报告。以下是 TaskListView.vue 的核心代码片段:

<template>
  <div class="task-list">
    <el-card>
      <template #header>
        <span>测试任务</span>
      </template>
      <el-table :data="tasks" v-loading="loading">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="planId" label="计划ID" width="100" />
        <el-table-column prop="status" label="状态" width="120">
          <template #default="scope">
            <el-tag :type="statusType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="threadCount" label="并发数" width="100" />
        <el-table-column prop="durationSeconds" label="持续时间" width="100" />
        <el-table-column prop="startedAt" label="开始时间" />
        <el-table-column prop="finishedAt" label="结束时间" />
        <el-table-column label="操作" width="180">
          <template #default="scope">
            <el-button size="small" @click="viewDetail(scope.row.id)">详情</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { taskApi, type TestTask } from '@/api/task';

const router = useRouter();
const tasks = ref<TestTask[]>([]);
const loading = ref(false);

const statusType = (status: string) => {
  switch (status) {
    case 'SUCCESS': return 'success';
    case 'FAILED': return 'danger';
    case 'RUNNING': return 'warning';
    default: return 'info';
  }
};

const loadTasks = async () => {
  loading.value = true;
  try {
    const res = await taskApi.list();
    tasks.value = res.data.data;
  } catch (error) {
    ElMessage.error('加载任务失败');
  } finally {
    loading.value = false;
  }
};

const viewDetail = (id: number) => {
  router.push(`/tasks/${id}`);
};

onMounted(() => {
  loadTasks();
});
</script>

7.6 路由配置

在 src/router/index.ts 中配置路由:

import { createRouter, createWebHistory } from 'vue-router';
import PlanListView from '@/views/PlanListView.vue';
import TaskListView from '@/views/TaskListView.vue';
import TaskDetailView from '@/views/TaskDetailView.vue';
import LoginView from '@/views/LoginView.vue';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', redirect: '/plans' },
    { path: '/login', component: LoginView },
    { path: '/plans', component: PlanListView },
    { path: '/tasks', component: TaskListView },
    { path: '/tasks/:id', component: TaskDetailView }
  ]
});

export default router;

7.7 开发代理与构建配置

在开发阶段,前端通过 Vite 的代理功能将 API 请求转发到后端服务。编辑 vite.config.ts:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets'
  }
});

开发模式下,运行 npm_run_dev 即可启动前端开发服务器。生产环境下,运行 npm_run_build 生成静态文件,将其部署到 Nginx 中。



八、平台部署:打包、systemd 与 Nginx 反向代理

完成前后端代码开发后,我们需要将整个平台部署到 Ubuntu 服务器上,并配置为可长期稳定运行的服务。本节将详细介绍数据库安装、后端打包、前端构建、systemd 服务管理以及 Nginx 反向代理配置。

8.1 安装 MySQL 数据库

测试平台需要持久化存储用户信息、测试计划、任务记录等数据。我们选择 MySQL 8.0 作为数据库。

安装 MySQL 服务器:

sudo apt update
sudo apt install -y mysql-server

安装完成后,启动 MySQL 并设置开机自启:

sudo systemctl start mysql
sudo systemctl enable mysql

运行 MySQL 安全初始化脚本,设置 root 密码并加固安全:

sudo mysql_secure_installation

按提示设置 root 密码、删除匿名用户、禁止 root 远程登录、删除测试数据库、重新加载权限表。

登录 MySQL 创建专用数据库和用户:

sudo mysql -u root -p

在 MySQL 命令行中执行:

CREATE DATABASE jmeter_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'jmeter'@'localhost' IDENTIFIED BY 'Jmeter@123456';
GRANT ALL PRIVILEGES ON jmeter_platform.* TO 'jmeter'@'localhost';
FLUSH PRIVILEGES;
EXIT;

这里创建了名为 jmeter_platform 的数据库,以及用户名为 jmeter、密码为 Jmeter@123456 的专用用户。请根据实际安全需求修改密码。

8.2 创建平台数据目录

测试平台需要存储上传的 JMX 文件、生成的 JTL 结果和 HTML 报告。建议创建专用的数据目录,并设置合适的权限:

sudo mkdir -p /var/lib/jmeter-platform/uploads
sudo mkdir -p /var/lib/jmeter-platform/reports
sudo chown -R jmeter-app:jmeter-app /var/lib/jmeter-platform
sudo chmod -R 755 /var/lib/jmeter-platform

8.3 后端打包

进入后端项目目录,使用 Maven 打包:

cd ~/projects/jmeter-platform
mvn clean package -DskipTests

打包完成后,在 target 目录下会生成 jmeter-platform-1.0.0.jar 文件。我们将这个 jar 文件复制到部署目录:

sudo mkdir -p /opt/jmeter-platform
sudo cp target/jmeter-platform-1.0.0.jar /opt/jmeter-platform/
sudo chown -R jmeter-app:jmeter-app /opt/jmeter-platform

8.4 前端构建

进入前端项目目录,安装依赖并构建生产版本:

cd ~/projects/jmeter-platform-ui
npm install
npm run build

构建完成后,会生成 dist 目录。将 dist 目录复制到 Nginx 的网站目录:

sudo mkdir -p /var/www/jmeter-platform
sudo cp -r dist/* /var/www/jmeter-platform/
sudo chown -R www-data:www-data /var/www/jmeter-platform

8.5 使用 systemd 管理后端服务

systemd 是 Ubuntu 系统的服务管理工具,通过配置 systemd 服务单元,我们可以实现后端服务的开机自启、自动重启和日志管理。

创建 systemd 服务文件:

sudo nano /etc/systemd/system/jmeter-platform.service

文件内容如下:

[Unit]
Description=JMeter Automation Testing Platform
After=network.target mysql.service

[Service]
Type=simple
User=jmeter-app
Group=jmeter-app
WorkingDirectory=/opt/jmeter-platform
ExecStart=/usr/bin/java -jar /opt/jmeter-platform/jmeter-platform-1.0.0.jar
Restart=on-failure
RestartSec=10
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
Environment=JMETER_HOME=/opt/jmeter

[Install]
WantedBy=multi-user.target

服务文件说明:

重新加载 systemd 配置,启动并启用服务:

sudo systemctl daemon-reload
sudo systemctl start jmeter-platform
sudo systemctl enable jmeter-platform

查看服务状态:

sudo systemctl status jmeter-platform

查看日志:

sudo journalctl -u jmeter-platform -f

如果服务启动失败,可以通过 journalctl 查看详细错误信息,常见问题包括数据库连接失败、端口被占用、JMeter 路径错误等。

8.6 配置 Nginx 反向代理

Nginx 负责对外提供统一的访问入口,将前端静态资源请求和 API 请求分别路由到正确的位置。这种配置方式可以避免浏览器跨域问题,也更符合生产环境部署习惯。

首先安装 Nginx:

sudo apt install -y nginx

创建 Nginx 配置文件:

sudo nano /etc/nginx/sites-available/jmeter-platform

配置文件内容如下:

server {
    listen 80;
    server_name _;  # 可改为实际域名或服务器 IP

    client_max_body_size 100M;

    # 前端静态资源
    location / {
        root /var/www/jmeter-platform;
        index index.html;
        try_files $uri $uri/ /index.html;
    }

    # 后端 API 代理
    location /api/ {
        proxy_pass http://localhost:8080/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 可选:静态报告目录代理
    location /reports/ {
        alias /var/lib/jmeter-platform/reports/;
        autoindex on;
    }
}

启用该站点配置:

sudo ln -s /etc/nginx/sites-available/jmeter-platform /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

注意:使用 proxy_pass http://localhost:8080/; 时,末尾的斜杠很重要。这样配置后,前端请求 /api/plans 会被转发到后端的 /plans。

如果默认的 Nginx 默认站点占用 80 端口,建议删除或禁用:

sudo rm /etc/nginx/sites-enabled/default
sudo systemctl reload nginx

8.7 部署后的验证清单

完成以上部署步骤后,建议按照以下清单进行验证:

检查项
验证方法
预期结果
MySQL 服务
sudo systemctl status mysql
状态为 active (running)
后端服务
sudo systemctl status jmeter-platform
状态为 active (running)
Nginx 服务
sudo systemctl status nginx
状态为 active (running)
API 接口
curl http://localhost/api/plans
返回 JSON 数据
前端页面
浏览器访问 http://服务器IP
显示平台登录或首页
文件上传
上传一个 JMX 文件
文件保存到 /var/lib/jmeter-platform/uploads




九、实战演示:从零执行一次完整的性能测试

经过前面的部署,平台已经具备了运行测试的基本能力。本节将通过一个完整的实战案例,演示如何设计测试计划、上传计划、配置执行参数、启动测试、查看结果和生成报告。

9.1 测试场景设计

假设我们要测试一个基于 Spring Boot 开发的用户查询接口,接口地址为 http://demo.example.com/api/users。测试目标是验证该接口在 50 并发用户、持续 5 分钟的情况下,能否保持稳定响应,以及系统的平均响应时间和吞吐量。

测试指标关注点:

9.2 创建 JMeter 测试计划

如果你有本地 JMeter GUI,可以按以下步骤创建测试计划。如果没有,可以直接使用我们提供的 JMX 模板内容,保存为 demo-user-api.jmx 后上传。

步骤 1:新建测试计划

打开 JMeter GUI,新建一个测试计划,命名为 “用户查询接口压力测试”。

步骤 2:添加线程组

右键点击测试计划,选择 “Add -> Threads (Users) -> Thread Group”。在线程组中配置:

为什么使用 ${__P(…)}?:通过 JMeter 属性函数,我们可以在启动测试时动态传入参数。例如 -JthreadCount=50 会覆盖默认值,这样平台就可以根据用户在前端填写的参数来灵活控制并发数和持续时间,而无需修改 JMX 文件。

步骤 3:添加 HTTP 请求

右键线程组,选择 “Add -> Sampler -> HTTP Request”。配置:

步骤 4:添加请求头

右键 HTTP 请求,选择 “Add -> Config Element -> HTTP Header Manager”。根据接口需求添加请求头,例如:

步骤 5:添加监听器

添加 “View Results Tree” 和 “Summary Report” 监听器用于调试。在 GUI 本地调试时使用,上传到服务器后不需要这些监听器(也可以保留,不影响 CLI 执行)。

步骤 6:保存为 JMX 文件

将测试计划保存为 demo-user-api.jmx

如果你不想使用 GUI,也可以直接创建以下 JMX 文件内容。这是一个简化版的 JMX 文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="用户查询接口压力测试" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="用户线程组" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">${__P(loopCount,-1)}</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">${__P(threadCount,10)}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${__P(rampUpSeconds,10)}</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${__P(durationSeconds,60)}</stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="用户查询接口" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">demo.example.com</stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/api/users</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

9.3 上传测试计划

打开浏览器,访问平台地址(例如 http://192.168.1.100)。登录后进入 “测试计划管理” 页面,点击 “上传计划” 按钮,选择 demo-user-api.jmx 文件,填写计划名称和描述,点击上传。

上传成功后,文件会被保存到 /var/lib/jmeter-platform/uploads 目录,同时在数据库的 test_plan 表中新增一条记录。你可以在服务器上查看确认:

ls -la /var/lib/jmeter-platform/uploads

9.4 配置并启动测试

在测试计划列表中,找到刚上传的 “用户查询接口压力测试” 计划,点击 “执行” 按钮。在弹出的对话框中配置参数:

点击 “启动” 后,前端会调用后端 API POST /api/tasks/execute/{planId},后端创建任务记录并启动异步执行。此时任务状态变为 RUNNING。

在后端日志中,可以通过 journalctl 实时查看 JMeter 的输出:

sudo journalctl -u jmeter-platform -f

在 JMeter 执行过程中,你可以在任务列表页面看到任务状态为 RUNNING。测试执行完成后,状态会变为 SUCCESS 或 FAILED。

9.5 查看测试结果与日志

测试执行完成后,点击任务列表中的 “详情” 按钮,可以查看任务的详细信息。后端会将 JMeter 的完整日志输出保存到 log_output 字段,并在详情页面展示。典型日志内容包括:

Creating summariser summary
Created the tree successfully using /var/lib/jmeter-platform/uploads/demo-user-api.jmx
Starting standalone test on Sat Jun 20 11:46:00 CST 2026 at 1624182360000
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 1234 in 00:00:10 = 123.4/s Avg: 45 Min: 12 Max: 234 Err: 0 (0.00%) Active: 50 Started: 50 Finished: 0
summary + 5678 in 00:00:30 = 189.3/s Avg: 52 Min: 15 Max: 456 Err: 2 (0.04%) Active: 50 Started: 50 Finished: 0
summary = 6912 in 00:00:40 = 172.8/s Avg: 50 Min: 12 Max: 456 Err: 2 (0.03%)
Tidying up ...
... end of run

日志中的关键信息:

9.6 查看 HTML 测试报告

JMeter 执行完成后,会自动生成 HTML 报告,保存到 /var/lib/jmeter-platform/reports/{uuid}/report 目录。报告中包含丰富的图表和统计信息,主要包括:

如果 Nginx 配置了 /reports/ 路径代理,可以直接通过浏览器访问:

http://192.168.1.100/reports/{uuid}/report/index.html

如果需要通过平台界面展示,可以在后端提供 API 返回报告路径,或直接读取报告中的关键指标存入数据库,前端展示聚合后的指标。

微信公众号二维码图片引自微信公众号,扫码关注阅读原文

9.7 结果分析与优化建议

拿到测试报告后,需要根据关键指标进行分析。以下是一些常见的分析维度和优化建议:

通过多次执行不同并发数的测试,可以绘制出系统的性能曲线,找到系统的性能瓶颈点和最大承载能力。



十、常见问题排查与优化建议

在搭建和运行 JMeter 自动化测试平台的过程中,可能会遇到各种各样的问题。本节总结一些常见问题和解决方案,帮助读者快速排查故障。

10.1 系统安装阶段问题

问题 1:安装过程中无法连接网络

如果在安装 Ubuntu 时无法连接网络,可能是网卡驱动不兼容或 DHCP 无法获取 IP。解决方法:

问题 2:SSH 无法连接

如果安装后无法通过 SSH 连接,可能原因:

10.2 Java 环境常见问题

问题 3:JMeter 提示找不到 Java

如果执行 jmeter -v 时提示找不到 Java,可能 JAVA_HOME 未正确设置。解决方法:

问题 4:Java 版本不匹配

JMeter 5.6 不支持 Java 8 以下版本,也不支持某些非常新的 Java 版本。建议统一使用 Java 17。如果系统中安装了多个 Java 版本,使用 update-alternatives 切换。

10.3 JMeter 执行常见问题

问题 5:JMeter 非 GUI 模式报错 “Cannot find .jmx file”

如果 JMeter 找不到 JMX 文件,可能原因:

问题 6:测试执行过程中出现 OutOfMemoryError

如果 JMeter 日志中出现 java.lang.OutOfMemoryError: Java heap space,说明堆内存不足。解决方法:

问题 7:HTML 报告生成失败

如果使用 -e -o 生成报告时失败,可能原因:

10.4 后端服务常见问题

问题 8:后端服务无法连接 MySQL

如果后端启动时报数据库连接错误,可能原因:

问题 9:JMeter 进程无法启动

如果后端调用 JMeter 时进程无法启动,可能原因:

问题 10:前端请求跨域或被 404

如果前端页面无法调用后端 API,可能原因:

10.5 性能优化建议

为了让平台能够稳定支撑大规模测试,以下是一些优化建议:

10.6 安全建议




十一、总结与展望

本文从系统安装开始,详细介绍了如何在 Ubuntu 26.04 Server 上部署一套完整的 JMeter 自动化测试服务平台。我们完成了以下工作:

这套平台虽然是一个基础版本,但已经涵盖了自动化测试平台最核心的功能。读者可以根据团队的实际需求进行扩展,例如:

性能测试平台建设不是一蹴而就的,需要根据实际业务场景持续迭代。希望本文能够为读者提供一个清晰的起点,帮助团队建立规范、高效的性能测试能力。如果在实践过程中遇到问题,欢迎参考本文的排查章节,或在 JMeter 社区和相关技术论坛中寻求帮助。

写在最后:自动化测试平台的价值不仅在于工具本身,更在于它承载的测试流程和团队协作方式。希望读者能够通过本文构建的平台,将性能测试融入日常研发流程,早发现、早优化,为用户提供更稳定、更快速的产品体验。