性能测试工具 JMeter 中脚本语言是通过什么方式操作线程中的变量的?

牧遥 · 2024年06月29日 · 3208 次阅读

JMeter 中脚本语言是通过什么方式操作线程中的变量的?

Java 语言中动态代码执行

在用 JMeter 做性能测试时,不可避免的需要操作线程中的参数,不同的接口需要接收的输入,以及他们响应结果都需要在运行过程中做动态处理。 JMeter 提供了 JSR223 取样器/前置处理器/后置处理器, 开发者可以在其中写脚本,那脚本时如何生效的?似乎在普通的 Web 开发中很少涉及到相关的内容。

JSR 233

JSR233 是一个标准/规范,它定义了一种接口,使得 Java 应用程序能够与脚本语言(如 JavaScript、Python、Ruby 等)进行互操作,在 Java 6 中引入,并通过 javax.script 包提供。

各自实现

所有希望自己的脚本语言能在 Java 环境中运行的开发者需要提供相应的脚本引擎实现。

例如:

​ JavaScript 脚本引擎,在 Java6 到 Java14 中包含在 JDK 中(不同的版本中可以一定是同一个引擎),Java15 以及以后就移除了了它,开发之需要在项目中单独引入。

​ groovy 脚本引擎,开发者如果需要使用 groovy 作为动态语言就需要单独引入相关的依赖,Maven 为例:

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-jsr223</artifactId>
    <version>3.0.21</version>
</dependency>
具体使用:以 Groovy 为例

1、 项目中引入依赖

2、 将 Groovy 脚本准备好,一个简单的算法,写入到任意一个文件中

import java.util.stream.Collectors

List<String> getMaxLengthStrings(String s) {
        String[] target = s.split("");
        List<String> storage = new ArrayList<>();
        StringBuilder transformable = new StringBuilder();
        for (int i = 0; i < target.length; i++) {
            transformable.append(target[i]);
            for (int j = i + 1; j < target.length; j++) {
                if (transformable.toString().contains(target[j])) {
                    break;
                }
                transformable.append(target[j]);
            }
            storage.add(transformable.toString());
            transformable = new StringBuilder();
        }
        // 获取最长不重复连续字符串,可能有多个
        List<String> result = storage.stream()
                .sorted(Comparator.comparing(String::length))
                .collect(Collectors.groupingBy(String::length))
                .get(storage.stream().max(Comparator.comparingInt(String::length)).get().length());
        return result;
    }

3、 启动 Java 程序,在程序运行过程中使用脚本

  • 直接调用顶级方法(全局函数)传参数,具体的处理逻辑在脚本中,返回处理后的结果
public static void main(String[] args) throws ScriptException, NoSuchMethodException, IOException {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("groovy");
    String groovyScript = FileUtils.readFileToString(new File("/Users/xxx/algorithm.log"), "UTF-8");
    engine.eval(groovyScript);
    Invocable invocable = (Invocable) engine;
    Object obj = invocable.invokeFunction("getMaxLengthStrings", "abcccccsdfioqccabc");
    if (obj instanceof List<?>) {
        System.out.println(obj);
    }
}
  • 解释:1、设置脚本引擎为 groovy , Java 程序允许会自动去接口中寻找 groovy 的实现类

​ 2、 使用 FileUtils(来自 commons-io 的实现 )方法读取本地文件中的代码脚本

​ 3、 调用 eval 方法执行脚本,此时会将脚本代码转换成中可运行的 Java 类,通过执行后返回当前类的运行结果

​ 4、通过 invokeFunction 调用脚本中全局函数, 指定函数名 getMaxLengthStrings, 指定入参: abcccccsdfioqccabc, 即可返回函数的运行结果

  • 直接调用顶级方法(全局函数),方法中处理的待对象在 Java 程序中准备好,返回处理后的结果
private static void test() throws ScriptException, NoSuchMethodException {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("groovy");
    List<String> list = new ArrayList<>();
    list.add("你好");
    Bindings bindings =  engine.createBindings();
    bindings.put("list", list);
    engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
    // list会使用Binding中设置的参数
    String groovyScript =   " List<String> getResult() {\n" +
                            "        list.add(\"Hello World\");\n" +
                            "        return list;\n" +
                            "    }";
    engine.eval(groovyScript, bindings);
    Invocable invocable = (Invocable) engine;
    // 调用全局函数
    Object obj = invocable.invokeFunction("getResult");
    if (obj instanceof List<?>) {
        System.out.println(obj);
    }
}
  • 解释:与上一个不一样的是, 指定了一个参数列表 Bindings , 通过 Bindings 中设置数据,用于 Java 代码和脚本之间的数据传递,Bindings 传递的对象是全局可用的, 可以全局函数中直接使用。

  • 调用脚本中类的特定方法,返回处理后的结果

private static void test2() throws ScriptException, NoSuchMethodException {
       ScriptEngine engine = new ScriptEngineManager().getEngineByName("groovy");
       List<String> list = new ArrayList<>();
       list.add("小明");
       Bindings bindings =  engine.createBindings();
       bindings.put("list", list);
       engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
       String groovyScript =   "class ScriptTest {\n" +
                               "    List list\n" +
                               "    ScriptTest(List list){\n" +
                               "        this.list = list\n" +
                               "    }\n" +
                               "    List<String> ScriptTestResult(){\n" +
                               "        list.add(\"小王\")\n" +
                               "        return list\n" +
                               "    }\n" +
                               "}\n" +
                               "scriptTest = new ScriptTest(list)";
       engine.eval(groovyScript, bindings);
       Invocable invocable = (Invocable) engine;
       Object ScriptTest = engine.get("scriptTest");
       Object obj = invocable.invokeMethod(ScriptTest,"ScriptTestResult");
       if (obj instanceof List<?>) {
           System.out.println(obj);
       }
   }
  • 解释: 与之前两种写法的区别是使用的类中的方法,没有直接使用全局函数,因此应该通过调用 invokeMethod 方法,第一个值是脚本对象,第二个值是方法名。Bindings 中的参数虽然是全局起作用的,但是由于 Groovy 脚本的作用域和类属性的不同管理方式,通常需要通过构造方法将这些绑定的值传递到类的实例中,以确保它们在类的方法中可以正常访问。
其他:
  • ScriptEngineManager:管理和发现可用的脚本引擎。

  • ScriptEngine:表示一个具体的脚本引擎实例,用于执行脚本代码。

  • Bindings:用于在脚本引擎和 Java 之间传递变量和对象。

  • ScriptContext:表示脚本作用范围(例如,ENGINE_SCOPEGLOBAL_SCOPE)。

性能
  • 尽管使用脚本引擎可以带来灵活性,但在性能要求较高的场景下,脚本代码的执行速度可能不如纯 Java 代码
安全性
  • 在执行动态脚本时,因为相当于直接往 Java 中插入了大段的代码,安全性遇到极大的挑战
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册