Skip to content

自定义插件

注意:插件的ApacheJMeter_coreApacheJMeter_java需要和jmeter版本对应,否则在启动测试时候报告错误。

自定义插件相关基础

设置变量到上下文

java
JavaSamplerContext.getJMeterContext().setVariables(new JMeterVariables() {{
    this.put("var1", value);
}});

AbstractJavaSamplerClient基础

AbstractJavaSamplerClient 是 Apache JMeter 中的一个抽象类,位于 org.apache.jmeter.protocol.java.sampler 包下。这个类为开发者提供了一个框架,用于实现自定义的 Java 采样器(Sampler)客户端,以便在 JMeter 性能测试中模拟和测试 Java 应用程序或服务的性能。以下是关于 AbstractJavaSamplerClient 的详细介绍:

主要方法和用途

AbstractJavaSamplerClient 包含了几个关键的方法,这些方法在自定义采样器时可能需要被重写或实现:

  1. setupTest(JavaSamplerContext context): 此方法在每个线程开始执行测试之前被调用一次,用于进行一些初始化操作,如资源分配、环境设置等。
  2. teardownTest(JavaSamplerContext context): 与 setupTest 相对应,此方法在每个线程结束测试之后被调用一次,用于执行清理工作,如资源释放、环境恢复等。
  3. getDefaultParameters(): 此方法返回一个 Arguments 对象,该对象定义了采样器在 JMeter GUI 中可配置的参数。通过此方法,开发者可以在 JMeter 的 Java 请求组件中展示自定义参数,并设置默认值。
  4. runTest(JavaSamplerContext context): 这是 AbstractJavaSamplerClient 中的核心方法,每个线程在每次迭代中都会调用此方法。开发者需要在此方法中实现具体的业务逻辑,如发送请求、接收响应、处理数据等,并返回一个 SampleResult 对象,该对象包含了测试执行的结果。

实现步骤

要在 JMeter 中使用 AbstractJavaSamplerClient 创建自定义的 Java 采样器,通常遵循以下步骤:

  1. 创建 Java 类:创建一个 Java 类,并继承 AbstractJavaSamplerClient。
  2. 实现方法:根据需要重写 setupTest、teardownTest、getDefaultParameters 和 runTest 方法。特别是,runTest 方法是必须实现的,因为它是采样器的核心功能所在。
  3. 编译和打包:将 Java 代码编译成 JAR 文件,并确保 JAR 文件中包含了所有必要的依赖项。
  4. 部署 JAR 文件:将编译好的 JAR 文件放置在 JMeter 的 lib/ext 目录下,这样 JMeter 就能加载并使用自定义的采样器了。
  5. 配置 JMeter:在 JMeter 中创建一个新的线程组,并添加一个 Java 请求。在 Java 请求中,配置类名称为自定义采样器的类名,并设置相应的参数。
  6. 运行测试:运行测试并查看结果,根据需要进行调整和优化。

注意事项

  • 在实现自定义采样器时,需要确保对 JMeter 的工作原理和 API 有一定的了解。
  • 自定义采样器的性能可能会影响整个测试的性能,因此需要仔细设计和优化。
  • 在部署 JAR 文件之前,请确保 JAR 文件中不包含任何潜在的安全风险或不必要的依赖项。

通过上述步骤和注意事项,开发者可以利用 AbstractJavaSamplerClient 在 JMeter 中创建功能强大且灵活的自定义 Java 采样器,以满足各种复杂的性能测试需求。

lib 和 lib/ext 目录

在JMeter中,liblib/ext目录扮演着重要的角色,它们与JMeter的运行和扩展功能密切相关。以下是关于这两个目录的详细解释:

lib目录

  • 作用lib目录是JMeter启动时的默认classpath,这意味着所有在JMeter运行过程中需要被引用的类或者jar包都必须存放在该目录下。
  • 内容:该目录下存放了JMeter运行所需的各种jar包和类文件,这些文件是JMeter核心功能的基础。
  • 注意事项:如果用户开发了新的JMeter组件或者需要添加额外的库文件(如JDBC驱动、Apache SOAP所需的jar包等),这些文件应该直接放在lib目录下,而不是lib/ext目录下。不过,用户的扩展所依赖的包如果要被JMeter自动发现,也需要放置在lib目录下。

lib/ext目录

  • 作用lib/ext目录用于存放JMeter的插件或者扩展组件。这些插件可以扩展JMeter的功能,使其能够支持更多的测试场景和协议。
  • 内容:该目录下通常包含了一些官方或社区提供的插件jar包。例如,用户可以从JMeter的插件官网(如jmeter-plugins.org)下载所需的插件,并将其放置在lib/ext目录下。
  • 插件管理:JMeter提供了插件管理的功能,用户可以在JMeter的GUI界面中查看已安装的插件、安装新的插件或卸载不需要的插件。在安装新插件时,用户只需将插件的jar包放置在lib/ext目录下,并重启JMeter即可。
  • 注意事项:虽然lib/ext目录用于存放插件,但并不意味着所有扩展功能都必须通过插件来实现。在某些情况下,用户也可以通过修改JMeter的配置文件或编写自定义的Java类来扩展其功能。

综上所述,liblib/ext目录在JMeter中扮演着不同的角色,但它们共同构成了JMeter功能扩展的基础。用户在使用JMeter时,应该根据需要合理地利用这两个目录来添加所需的库文件和插件,以便更好地满足测试需求。

实现自定义插件

详细用法请参考本站 示例

POM 添加如下配置:

xml
<properties>
    <jmeter.version>5.6.2</jmeter.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
xml
<dependency>
    <groupId>org.apache.jmeter</groupId>
    <artifactId>ApacheJMeter_core</artifactId>
    <version>${jmeter.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.jmeter</groupId>
    <artifactId>ApacheJMeter_java</artifactId>
    <version>${jmeter.version}</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-json</artifactId>
    <version>5.8.28</version>
</dependency>
xml
<build>
    <plugins>
        <!-- 使用下面插件打包包括所有依赖的jar -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <id>assemble-all</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

编写插件

java

/**
 * 测试 JSON 解码性能
 */
public class JSONDecodeSampler extends AbstractJavaSamplerClient {
    private static final Logger log = LoggerFactory.getLogger(JSONDecodeSampler.class);

    final static String JSON = "{\"errorCode\":0,\"errorMessage\":null,\"dataObject\":\"你好\"}";

    // 线程每个测试loop调用
    @Override
    public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
        SampleResult result = new SampleResult();
        try {
            // 样本测试开始,自动统计样本持续测试时间,不需要手动计算
            result.sampleStart();

            JSONObject jsonObject = JSONUtil.parseObj(JSON);
            int errorCode = jsonObject.getInt("errorCode");

            if (errorCode > 0) {
                // 注入变量registerSuccess,能够使用vars.get("registerSuccess")获取变量值
                javaSamplerContext.getJMeterVariables().put("registerSuccess", "false");
            } else {
                javaSamplerContext.getJMeterVariables().put("registerSuccess", "true");
            }

            // 设置发送数据长度
            result.setSentBytes(5);

            // 设置sample label
            result.setSampleLabel("我的样品");
            // 设置request headers
            result.setRequestHeaders("设置请求头值");
            // 设置request body
            // https://stackoverflow.com/questions/74187155/how-to-populate-request-request-body-tab-of-a-jmeter-sampler-result-displa
            result.setSamplerData("设置请求body内容");

            // 用于协助测试是否会打印错误日志
            /*boolean b = true;
            if(b) {
                throw new Exception("99999999");
            }*/

            // 标记样本成功
            result.setSuccessful(true);
            // 设置样本请求成功
            result.setResponseOK();
        } catch (Exception e) {
            log.error(e.getMessage(), e);

            // 标记样本失败
            result.setSuccessful(false);
            result.setResponseMessage("发生错误,原因: " + e.getMessage());
            result.setResponseCode("50000");
        } finally {
            // 不使用responseData数据长度时手动设置received数据长度
            /*result.setBytes(5L);
            result.setBodySize(5L);*/

            // 设置response headers
            result.setResponseHeaders("设置response headers");
            // 设置response body
            // 设置相应数据,会自动计算received数据长度
            result.setResponseData("123456".getBytes(StandardCharsets.UTF_8));
            result.setDataType(SampleResult.TEXT);

            result.sampleEnd();
        }
        return result;
    }

    // 测试线程启动时调用
    @Override
    public void setupTest(JavaSamplerContext context) {
        super.setupTest(context);
        // 注意:log.debug不会打印日志,猜测可能需要调整jmeter日志等级配置
        // 在jmeter控制台或者jmeter.log日志文件中打印日志
        log.info("线程启动");
    }

    // 测试线程退出时调用
    @Override
    public void teardownTest(JavaSamplerContext context) {
        super.teardownTest(context);
        log.info("线程退出");
    }

    // 用于定义采样器在JMeter GUI中可用的参数。当你将自定义采样器添加到JMeter的测试计划中,并尝试配置它时,这些参数就会显示在“添加/编辑”对话框中。
    // getDefaultParameters() 方法应该返回一个 Arguments 对象,该对象包含了采样器需要的所有参数的定义。这些参数可以包括名称、默认值、描述等信息。
    @Override
    public Arguments getDefaultParameters() {
        return new Arguments() {{
            // 通过context.getParameter("k1")参数设置值
            addArgument("k1", "v1");
            addArgument("k2", "v2");
        }};
    }
}

编译和部署插件

bash
./deploy-plugin.sh

deploy-plugin.sh 脚本如下:

bash
#!/bin/bash

set -e
set -x

# 编译插件
mvn package

sudo rm -f /usr/local/jmeter/lib/ext/demo-jmeter-customize*.jar

# 复制插件到jmeter lib/ext目录
sudo cp ./target/demo-jmeter-customize-plugin-1.0.0-jar-with-dependencies.jar /usr/local/jmeter/lib/ext/demo-jmeter-customize-plugin-1.0.0.jar

# 复制插件依赖的库到jmeter lib/ext目录,否则会报告ClassNotFound错误
#sudo cp ./target/required-lib/*.jar /usr/local/jmeter/lib/ext/
#sudo chown root:root /usr/local/jmeter/lib/ext/*.jar

# 修改插件属主为root
sudo chown root:root /usr/local/jmeter/lib/ext/demo-jmeter-customize*.jar

创建 jmeter.jmx 测试计划文件

xml
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.2">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" 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="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <intProp name="LoopController.loops">-1</intProp>
          <boolProp name="LoopController.continue_forever">false</boolProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">16</stringProp>
        <stringProp name="ThreadGroup.ramp_time">0</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
        <boolProp name="ThreadGroup.delayedStart">false</boolProp>
      </ThreadGroup>
      <hashTree>
        <JavaSampler guiclass="JavaTestSamplerGui" testclass="JavaSampler" testname="Java Request" enabled="true">
          <elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="k1" elementType="Argument">
                <stringProp name="Argument.name">k1</stringProp>
                <stringProp name="Argument.value">v1</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="k2" elementType="Argument">
                <stringProp name="Argument.name">k2</stringProp>
                <stringProp name="Argument.value">v2</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="classname">com.future.demo.JSONDecodeSampler</stringProp>
        </JavaSampler>
        <hashTree/>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="false">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

启动测试

bash
jmeter -n -t jmeter.jmx