文章目录
简介
Jmeter是一个非常强大的测试工具,功能非常全,也非常灵活,灵活的地方在于几乎所有的组件都可以通过脚本自定义。
本文就主要介绍一下Jmeter的脚本语言的原理,方便大家在使用Jmeter的时候,可以更好的理解和编写脚本。
Jmeter支持很多语言,但是官方推荐的还是Groovy,所有我们会以Groovy为例。
原理
其实,Jmeter是使用Java实现的,它执行脚本本质就是通过ScriptEngineManager获取ScriptEngine来执行脚本的。
具体的源码可以看:
org.apache.jmeter.JMeter
org.apache.jmeter.functions.JavaScript
简单示例
一个非常简单的例子:
java
@Test
public void base() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
engine.eval("println 'Hello from Groovy using ScriptEngine!'");
engine.eval("def name = 'World'; println 'Hello, ' + name + '!'");
}
上面就是获取了groovy的ScriptEngine,然后通过eval执行了groovy代码,是不是非常简单。
当然,需要引入对应版本的groovy依赖:
xml
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.25</version>
<type>pom</type>
</dependency>
在Jmeter中已经帮我们引入好了,不需要我们操心。

注入实例变量
在Jmeter中脚本中,我们经常会使用到一些Jmeter的实例变量,比如:
- log
- ctx
- vars
- props
- threadName
- sampler
- sampleResult
这些其实都是Jmeter在执行脚本之前,已经帮我们注入到脚本中的,我们只需要在脚本中直接使用。
这就是Groovy脚本的强大之处,他完全兼容Java,所以我们可以直接使用这些变量示例。
java
@Test
public void evalWithParam() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
Bindings bindings = engine.createBindings();
User user = new User();
bindings.put("arg", "hello word");
bindings.put("user", user);
String script = """
println arg
println user.hi("world")
""";
ScriptContext newContext = new SimpleScriptContext();
newContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
Object result = engine.eval(script, newContext);
if(result != null) {
String resultStr = result.toString();
System.out.println(resultStr);
}
}
@Data
private static class User {
private Long id;
private String name;
private Date birthday;
public String hi(String name){
return "hi " + name + " !";
}
}

操作文件
在脚本中,最常用的操作就是解析json、操作文件。
Groovy也支持非常好,可以使用JsonSlurper解析json,使用JsonOutput将对象转换为json字符串。
java
@Test
public void dealJson() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
Bindings bindings = engine.createBindings();
User user = new User();
user.setId(88L);
user.setName("allen");
user.setBirthday(new Date());
bindings.put("arg", "hello word");
bindings.put("user", user);
String script = """
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
def jsonString = '{"name":"John", "age":30}'
def jsonSlurper = new JsonSlurper()
def jsonObject = jsonSlurper.parseText(jsonString)
println jsonObject.name
println jsonObject.age
def person = [name: 'John', age: 30]
def resultJsonString = JsonOutput.toJson(person)
println resultJsonString
resultJsonString = JsonOutput.toJson(user)
println(resultJsonString)
def writeFile = new File("F:\\\\tmp.json");
writeFile.withWriter {
writer->
writer.write(resultJsonString)
}
def readFile = new File("F:\\\\tmp.json");
println "readFile.text: " + readFile.getText()
return "success"
""";
ScriptContext newContext = new SimpleScriptContext();
newContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
Object result = engine.eval(script, newContext);
if(result != null) {
String resultStr = result.toString();
System.out.println(resultStr);
}
}
JavaScript的问题
使用Jmeter不推荐使用Groovy之外的脚本语言,因为支持不好,效率不高。
对于JavaScript,Jmeter默认使用的是nashorn引擎。
但是高版本的JDK15+已经移除了nashorn引擎。
怎么办呢?
对于Jmeter,降低JDK版本,使用JDK15一下的版本。
自己测试可以引入:
xml
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>23.0.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>23.0.0</version>
</dependency>
java
@Test
public void javaScript() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptContext newContext = new SimpleScriptContext();
// ScriptEngine engine = manager.getEngineByName("nashorn");
// ScriptEngine engine = manager.getEngineByName("graal.js");
ScriptEngine engine = manager.getEngineByName("js");
Bindings bindings = engine.createBindings();
String script = """
console.log(arg);
let upperCaseString = arg.toUpperCase();
console.log(upperCaseString);
console.log(user);
""";
User user = new User();
user.setId(88L);
user.setName("allen");
user.setBirthday(new Date());
bindings.put("arg", "hello word");
bindings.put("user", user);
newContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
Object result = engine.eval(script, newContext);
if(result != null) {
String resultStr = result.toString();
System.out.println("调用结果:" + resultStr);
}
}
非常不友好,不能直接调用对象方法,所以尽量选Groovy语言吧。
会Java的朋友的福音
有朋友可能会说我不想学groovy,怎么办呢?
对于会java的朋友,可以直接把groovy脚本当做java代码来写。
groovy会默认导入下面的包:
java
java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.*
Jmeter的lib包中已经引入了httpclient、jackson、json-path、commons-lang3、jsoup、xstream等包,这些库都可以直接用。
例如,我们可以使用下面的代码来读取文件内容:
java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("F:\\tmp.json");
String content = Files.readString(path);
log.info(content);
Groovy默认导入的是java.io,所以使用nio的包,我们手动导入一下就可以。

所以,完全可以在IDE中写完Java代码,直接拷贝过去就可以。
当然,还是得注意依赖问题。
需要啥包,可以直接下载了忘Jmeter安装目录下的lib目录下扔就可以。
Grab
当然也可以使用Grab,它可以自动下载依赖包,会放在用户目录下的.groovy/grapes目录下。
可以通过System.setProperty("grape.root", "xxxx/.groovy/grapes")设置
Grab虽然方便,它及不兼容Gradle也不兼容Maven,而是使用ivy-xxx.jar,例如ivy-2.5.3.jar,有它直接的格式,有点问题就很难处理。
所以,对于Jmeter最方便的还是直接拷贝jar包到安装目录下的lib目录,然后重启Jmeter,就可以使用了。
groovy
// @GrabResolver(name='aliyun', root='https://maven.aliyun.com/repository/public')
// @GrabConfig(systemProperties=['http.proxyHost=198.185.120.100', 'http.proxyPort=7000','https.proxyHost=198.185.120.100', 'https.proxyPort=7000','systemProp.https.proxyUser=xxx','systemProp.https.proxyPassword=pppp','systemProp.http.proxyUser=xxx','systemProp.http.proxyPassword=ppp'])
@Grab('com.alibaba.fastjson2:fastjson2:2.0.59')
import com.alibaba.fastjson2.JSON;
class User {
private Integer age;
private String name;
Integer getAge() {
return age
}
void setAge(Integer age) {
this.age = age
}
String getName() {
return name
}
void setName(String name) {
this.name = name
}
}
User user = new User();
user.setAge(18);
user.setName("tim");
String jsonString = JSON.toJSONString(user);
log.info("jsonString:{}", jsonString);
注意:Grab依赖apache的ivy,必须先有这个包才能自动下载
Jmeter注入变量
前面我们已经介绍了,Jmeter会在执行脚本之前先注入一些上下文信息,方便我们在脚本访问和引用。
但实际上不同的生命周期,我们能获取到的对象是不一样的,最典型的就是在后置处理器中通过SampleResult不能获取到结果。
是因为在后置处理器中,采样器的结果,已经算是前一个采样器的结果了,所以得通过ctx.getPreviousResult()或者prev获取。
- log:org.slf4j.Logger,记录日志的,实际使用的实现是log4j,配置文件为:安装目录/bin/log4j2.xml
- ctx:org.apache.jmeter.threads.JMeterContext,可以获取采样器、前一个采样器、采用结果、变量、线程组等信息
- vars:org.apache.jmeter.threads.JMeterVariables,最常用,最重要的,我们的变量设置获取就靠它
- props:java.util.Properties,Jmeter的一些属性信息,例如数据库驱动等
- sampler:org.apache.jmeter.samplers.Sampler,当前的采样器
- SampleResult:org.apache.jmeter.samplers.SampleResult,采样结果信息
- args:String,参数
- Parameters,String,参数
- FileName:文件名称
- prev:SampleResult,前一个采用器的结果
- Label:组件名称
- OUT:输出到Jmeter控制台,java.io.PrintStream
- AssertionResult:断言结果
这些信息都可以在脚本中查看,例如,我们一个数据库连接不上了,我们就可以通过props看一下是不是驱动有问题啊。
groovy
props.each { key, value ->
log.info("$key = $value")
}

这么多,很可能还会变,靠记肯定不靠谱,大致了解即可,知道有哪些东西,具体要用的时候,在脚本中打印一下可用的绑定key,和对应的类信息,然后去Jmeter的源码看有哪些方法可以使用:
groovy
log.info("可用绑定键: " + binding.getVariables().keySet());
log.info("OUT:{},{}",OUT.getClass(),OUT.hashCode());
OUT.println("OUT println " + OUT.getClass())
Jmeter脚本实例
预处理参数
使用Jmeter脚本一个很重要的原因就是要处理各种参数,例如我们要压测大文件的场景,需要一个很大的json传给服务器,直接拷贝内容进body好像不太方便,这个时候就可以直接使用Jmeter的脚步来处理这种情况。
还有很多时候,我们多种场景需要从csv或者Excel中读取数据,可以通过Jmeter函数的方式实现,但是没有脚本的方式方便。
比如,我们有个一user.json内容如下:
json
[{"id":0,"name":"tim_0"},{"id":1,"name":"tim_1"},{"id":2,"name":"tim_2"}]
怎么才能把这个文件的内容,作为请求的body参数传递给服务器呢?
很简单:
- 准备脚本,读取文件内容
- 通过Jmeter注入的vars对象把参数保存起来
- 在采样器中通过${param}的方式引用内容
groovy
def readFile = new File("D:\\tmp\\user.json")
def param = readFile.getText()
log.info("读取文件参数:{}",param)
vars.put("jsonParam",param)

提取返回结果
服务端往往有自己的业务状态码,Jmeter HTTP请求默认会使用http的状态码来判断成功失败。
如何处理呢,我们可以通过后置处理器脚本来处理。
Jmeter lib中包含了jackson,所以可以直接使用。
groovy
import com.fasterxml.jackson.databind.ObjectMapper;
def body = prev.getResponseDataAsString()
log.info("服务器返回值:{}",body)
class Result{
private Boolean success;
private String message;
private Integer code;
private Object data;
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
ObjectMapper objectMapper = new ObjectMapper();
Result result = objectMapper.readValue(body, Result.class);
log.info("服务端code:{},服务端message:{}",result.code,result.message)
获取参数
我们可以通过sampler.getArguments()获取参数:
Query Data: id=123&name=J36
{"id":0,"name":"tim_0"},{"id":1,"name":"tim_1"},{"id":2,"name":"tim_2"}
groovy
def readFile = new File("D:\\tmp\\user.json")
def param = readFile.getText()
log.info("读取文件参数:{}",param)
vars.put("jsonParam",param)
log.info("sampler:{},{}",sampler.getClass(),sampler)
log.info("getArguments:{}",sampler.getArguments())
header处理
除了通过HTTP请求头配置原件,还可以通过脚本的方式设置请求头:
groovy
import org.apache.jmeter.protocol.http.control.Header
import org.apache.jmeter.protocol.http.control.HeaderManager
def readFile = new File("D:\\tmp\\user.json")
def param = readFile.getText()
log.info("读取文件参数:{}", param)
vars.put("jsonParam", param)
def hm = sampler.getHeaderManager()
log.info("hm:{}", hm)
// 预处理阶段还没有创建,手动创建
if (hm == null) {
hm = new HeaderManager();
sampler.setHeaderManager(hm);
}
hm.add(new Header("Content-Type", "application/json"))
接口签名
很多接口是有参数签名的,这种要怎么处理呢?
放心,Groovy还是能轻松处理这个问题,下面就是一个比较通用的规范实现,不侵入业务,基本拿来就能用,最多需要改一下参数名称。
对于接口签名不清楚的可以看一下:接口参数签名核心问题
groovy
import org.apache.jmeter.protocol.http.control.Header
import org.apache.jmeter.protocol.http.control.HeaderManager
// Jmeter的lib包已经包含了apache common codec依赖的jar包,直接可以用
import org.apache.commons.codec.digest.DigestUtils
// 首先我们得自己添加时间戳和nonce这2个非业务参数
def nonce = UUID.randomUUID().toString()
def timestamp = System.currentTimeMillis()
sampler.addArgument("timestamp",String.valueOf(timestamp))
sampler.addArgument("nonce",nonce)
// 获取所有参数,包括我们添加到timestamp、nonce以及实际业务参数
def arguments = sampler.getArguments()
def argMap = arguments.getArgumentsAsMap()
// 签名密钥,不用传输只是参与计算签名
argMap.put("appSecret","123456qdd")
def treeMap = new TreeMap<String, String>(argMap)
StringJoiner stringJoiner = new StringJoiner("&")
Set<Map.Entry<String, String>> entries = treeMap.entrySet()
for (Map.Entry<String, String> entry : entries) {
stringJoiner.add(entry.getKey() + "=" + entry.getValue())
}
String paramsStr = stringJoiner.toString()
def sign = DigestUtils.sha256Hex(paramsStr)
def hm = sampler.getHeaderManager()
if (hm == null) {
hm = new HeaderManager();
sampler.setHeaderManager(hm);
}
// 通常将签名放在header中,不和参数混在一起、避免额外处理
hm.add(new Header("sign",sign))
创建JSR223前置处理器,使用上面脚本就可以:

签名结果:

接口的示例代码:
java
@RequestMapping("/sign")
public Result<?> sign(@RequestHeader("sign") String sign, @RequestParam Map<String, String> param) {
String appSecret = "123456qdd";
param.put("appSecret", appSecret);
boolean flag = SignHelper.verifySign(sign, param);
if (flag) {
return Results.ofSuccess();
} else {
return Results.ofFail("非法请求");
}
}