Programmatic Test Plans & DSLs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
The reference material consulted while preparing these pages is listed in the section bibliography.
This page was written against Apache JMeter 5.6.3 (latest release on 2026-10-14; it runs on Java 8 and later, Java 17
or later is recommended, and Java 21 is used here), with the differences planned for JMeter 6.0 noted where they
matter. JMeter 6.0 is not released: its planned changes come from xdocs/changes.xml on the master branch, need Java 17
or later (Java 21 recommended) and are described as planned, not as shipped behaviour. The third-party
JMeter Java DSL by Abstracta is covered in version 2.3 (published to Maven Central on 2026-10-07, built on
JMeter 5.6.3, Apache License 2.0). The JMeter Plugins Manager 2.0 and the jmeter-maven-plugin 3.8.x are mentioned only
where code-based plans meet them. Every Java example on this page was compiled and run on Java 21 against JMeter 5.6.3
(through the installed distribution and through the Maven artifacts) and JMeter Java DSL 2.3, against a local stub
server; the outputs shown are from those runs.
A JMeter test plan is usually a .jmx file built in the GUI. It does not have to be: the plan is a tree of Java
objects, and code can build, save, load and run that tree. JMeter 5.6 made this an official, if experimental, feature
with a Kotlin and Java DSL and a GUI action that turns any part of a plan into code. Independently, the open-source
JMeter Java DSL wraps JMeter in a fluent Java API designed for performance tests that live next to unit tests, run with
JUnit 5 and fail the build with ordinary assertions. This page covers both routes, builds parts of the Bookshelf plan
with each, and ends with when a .jmx file is still the better choice.
It assumes the plan model from Test Plan Elements, Execution Order & Scoping, the run properties from Properties, Variables & Functions and the CLI from Running Tests in CLI Mode. The Spring Boot side of performance testing is in Performance Testing with Apache JMeter, and functional tests in Unit and Integration Testing.
Why build test plans in code
A .jmx file is XML written by the GUI. It works well for people who design plans visually, but it has known costs
when a team treats load tests as code:
-
Reviews and merges. A one-field change in the GUI can rewrite many lines of XML, and two people editing the same plan produce merge conflicts that are hard to resolve by hand.
-
Reuse. The GUI offers Test Fragments, Module Controllers and Include Controllers; code offers functions, parameters, loops and libraries, which are more flexible.
-
Generation. Some plans are naturally generated: one sampler per endpoint of an OpenAPI document, one thread group per tenant, a plan per environment.
-
Assertions on the whole run. JMeter’s assertions judge single samples. Pass or fail on aggregate numbers (95th percentile, error rate) needs a post-processing step in CI; a test framework can do it directly.
The costs of code are just as real: the plan is no longer visible to people who only know the GUI, not every JMeter element or plugin has a builder, and the API of JMeter itself is marked experimental. The last section of the page weighs both sides.
Three routes, one engine
All routes end in the same place: a ListedHashTree of TestElement objects handed to StandardJMeterEngine, the
engine that jmeter -n uses. They differ in who writes the tree.
-
The GUI and the CLI: the GUI writes
.jmx;jmeter -n -tloads it withSaveServiceand runs it. -
JMeter’s own API: code creates
TestElementobjects and arranges them in aListedHashTree, by hand or with the 5.6testTreeDSL.SaveServicecan write that tree as a.jmx. -
JMeter Java DSL: code calls static builders such as
testPlan(...)andthreadGroup(...); the library builds the tree, runs it in an embedded engine and returns statistics. It can also save and load.jmxfiles.
JMeter’s own API
The object model: elements, trees and order
Every node of a plan (Test Plan, Thread Group, sampler, timer, listener) is a TestElement: a Java object that holds
its configuration as named properties, the same properties that appear as stringProp, intProp and boolProp in
the .jmx. The elements do not point at their children. The manual is explicit that "in JMeter APIs, the elements do
not form a tree": parent and child relations live in a separate structure, the ListedHashTree from
org.apache.jorphan.collections.
-
ListedHashTree.add(element)adds a node and returns the subtree of that node, so children are added to the returned value. -
ListedHashTreekeeps insertion order. The plainHashTreedoes not, so samplers built into aHashTreemay run in a shuffled order. The manual warns against confusing the two. -
Execution order, scoping and the rule that timers and assertions apply to the samplers in their scope are the same as for a plan built in the GUI, because the engine sees the same tree.
Building a plan with the low-level API
The manual’s first example builds Test Plan, Thread Group and Debug Sampler by hand. The version below is the same
idea with the declared types corrected (the manual’s snippet declares the thread group variable as TestPlan, which
does not compile):
ListedHashTree root = new ListedHashTree(); // (1) the tree
TestPlan testPlan = new TestPlan();
ListedHashTree testPlanSubtree = root.add(testPlan); // (2) add() returns the subtree
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.setName("Search Order Thread Group");
ListedHashTree threadGroupSubtree = testPlanSubtree.add(threadGroup); // (3) child of the Test Plan
DebugSampler debugSampler = new DebugSampler();
threadGroupSubtree.add(debugSampler); // child of the Thread Group
This works, but nesting is expressed by juggling subtree variables, and nothing in the code looks like the tree it builds. That is what the 5.6 DSL fixes.
The 5.6 Kotlin DSL: testTree { ... }
JMeter 5.6 (June 2023) added the class org.apache.jmeter.treebuilder.TreeBuilder and the function
org.apache.jmeter.treebuilder.dsl.testTree, contributed in PR 678 and listed in the 5.6 changes as an "experimental
Kotlin and Java DSL for programmatic test plan generation". In Kotlin the code nests exactly like the plan:
import org.apache.jmeter.sampler.DebugSampler
import org.apache.jmeter.testelement.TestPlan
import org.apache.jmeter.threads.ThreadGroup
import org.apache.jmeter.treebuilder.dsl.testTree
val root = testTree { // (1) creates a TreeBuilder and returns its ListedHashTree
TestPlan::class { // (2) creates a TestPlan and opens its children block
ThreadGroup::class {
name = "Search Order Thread Group" // the block's receiver is the new ThreadGroup
+DebugSampler::class // (3) child without a block: unary plus
+DebugSampler() // (4) an instance you created yourself
}
}
}
The operations of TreeBuilder in JMeter 5.6.3, read from its source:
| Kotlin form | Java form | What it does |
|---|---|---|
|
|
Creates the element with its no-argument constructor, adds it under the current node, runs the block with the element as receiver (Kotlin) or parameter (Java), so elements added inside the block become its children. |
|
|
Creates and adds an element that has no children and needs no configuration. |
|
|
Adds an instance already created and configured in code. |
|
|
Adds an existing instance and gives it children. |
|
|
Registers an action that runs on every element of that type (or subtype) added later in the current scope, after the element’s own block. Useful for defaults such as timeouts. |
|
|
The same for every element in the scope. |
|
same names as getters |
Read-only access to the resulting tree and to the position of the builder. |
Two details matter. The builder sets the testClass property of each element to its Java class if it is not set
already, which SaveService needs. It does not set guiClass, the property the GUI uses to choose the editor for
an element; set it yourself if the .jmx will be opened in the GUI (see the complete example below).
The manual also shows how to factor a recurring pattern into an extension function of TreeBuilder, for example a
thread group that always carries a Summariser. Applied to Bookshelf, a reusable search thread group looks like this:
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy
import org.apache.jmeter.reporters.Summariser
import org.apache.jmeter.testelement.TestPlan
import org.apache.jmeter.threads.ThreadGroup
import org.apache.jmeter.treebuilder.Action
import org.apache.jmeter.treebuilder.TreeBuilder
import org.apache.jmeter.treebuilder.dsl.testTree
fun TreeBuilder.threadGroup( // (1) an extension of TreeBuilder
name: String,
numThreads: Int = 1,
rampUp: Duration = 1.seconds,
body: Action<ThreadGroup>
) {
ThreadGroup::class { // (2) the normal DSL inside
this.name = name
this.numThreads = numThreads
this.rampUp = rampUp.inWholeSeconds.toInt()
+Summariser::class
body(this) // (3) the caller's children
}
}
val plan = testTree {
TestPlan::class {
threadGroup(name = "search", numThreads = 5) { // (4) used like a built-in element
HTTPSamplerProxy::class {
name = "search books"
domain = "localhost"
port = 8080
path = "/api/books"
method = "GET"
addArgument("query", "dune")
}
}
}
}
A Thread Group created this way has no loop controller yet; the complete Java example below shows how to add one.
The Kotlin compiler is not part of JMeter, so Kotlin plans need a Kotlin build (Gradle or Maven with the Kotlin
plugin); JMeter 5.6.3 ships the Kotlin runtime (kotlin-stdlib 1.9.22 in lib/).
The 5.6 Java DSL: TreeBuilders.testTree
The same builder works from Java through the static method org.apache.jmeter.treebuilder.dsl.TreeBuilders.testTree.
Java has no receivers, so every addition goes through the builder reference b, and each lambda receives the new
element as a parameter:
import static org.apache.jmeter.treebuilder.dsl.TreeBuilders.testTree;
import org.apache.jmeter.sampler.DebugSampler;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.threads.ThreadGroup;
import org.apache.jorphan.collections.ListedHashTree;
ListedHashTree root = testTree(b -> { // (1) b is the TreeBuilder
b.add(TestPlan.class, tp -> { // (2) tp is the new TestPlan
b.add(ThreadGroup.class, tg -> { // always add through b, never through tp or tg
tg.setName("Search Order Thread Group");
b.add(DebugSampler.class); // (3) no children: no lambda
b.add(new DebugSampler()); // (4) an existing instance
});
});
});
The lambdas are of type org.apache.jmeter.treebuilder.Action, a JMeter interface rather than java.util.function.Consumer,
so the same API reads naturally from Kotlin and Java.
Generating code from the GUI: Copy Code
The GUI writes the DSL for you. Right-click any element in the tree and choose Copy Code (in the context menu, after Save Selection As… and Save as Test Fragment). JMeter copies Kotlin DSL for that element and all its children to the clipboard; with several elements selected it copies them one after another. The action was added in 5.6 with the DSL.
The generated code uses the long class names and the property schema introduced in 5.6 (props { it[...] = ... }).
For the HTTP Request in the complete example below, the output is:
org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy::class {
props {
it[arguments] = org.apache.jmeter.config.Arguments().apply {
props {
it[arguments] = listOf(
org.apache.jmeter.protocol.http.util.HTTPArgument().apply {
props {
it[alwaysEncode] = true
it[argumentName] = "query"
it[value] = "dune"
it[metadata] = "="
}
},
)
}
}
it[name] = "search books"
it[guiClass] = "org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui"
it[protocol] = "http"
it[domain] = "localhost"
it[port] = 8080
it[path] = "/api/books"
it[method] = "GET"
it[followRedirects] = true
it[useKeepalive] = true
}
}
What to know about the action, from its 5.6.3 implementation (CopyCodeAction and DslPrinterTraverser):
-
Kotlin only. There is no Java output; translate by hand, using the table above.
-
Only non-default properties are printed: a property is skipped when a freshly constructed element has the same value. The output is a faithful copy of what the GUI stored, not a tidy hand-written plan.
-
Secrets are copied too. The manual’s own example shows a proxy user name and password in the generated code. Clear credentials from the plan, or from the output, before committing it.
-
The same printer is public.
new DslPrinterTraverser()passed totree.traverse(...)prints any tree as Kotlin DSL, which the complete example uses to show what it built.
Complete example: build, save and run the Bookshelf search plan
The program below builds a one-sampler Bookshelf search plan with the Java DSL, saves it as bookshelf-search.jmx,
prints it as Kotlin DSL, and runs it with a summariser and a JTL file, all without the jmeter script. The steps that
bin/jmeter normally performs are explicit:
| Step | Why it is needed |
|---|---|
|
Elements look up files (such as |
|
Loads the defaults that every element reads.
Plan properties such as |
|
Initialises messages and the locale used for output. |
|
Write and read |
|
Without them nothing is printed or written: the CLI adds them for |
|
Starts the test on a background executor;
|
import static org.apache.jmeter.treebuilder.dsl.TreeBuilders.testTree;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.time.Duration;
import org.apache.jmeter.control.LoopController;
import org.apache.jmeter.control.gui.LoopControlPanel;
import org.apache.jmeter.control.gui.TestPlanGui;
import org.apache.jmeter.dsl.DslPrinterTraverser;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.reporters.Summariser;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.threads.ThreadGroup;
import org.apache.jmeter.threads.gui.ThreadGroupGui;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.ListedHashTree;
public class BookshelfSearchPlan {
public static void main(String[] args) throws Exception {
String jmeterHome = args[0]; // e.g. /opt/apache-jmeter-5.6.3
int threads = Integer.parseInt(args.length > 1 ? args[1] : "5");
int port = Integer.parseInt(args.length > 2 ? args[2] : "8080");
// 1. Initialise JMeter: home, properties, locale (what bin/jmeter does for you)
JMeterUtils.setJMeterHome(jmeterHome);
JMeterUtils.loadJMeterProperties(jmeterHome + "/bin/jmeter.properties");
JMeterUtils.initLocale();
// 2. Build the tree with the experimental 5.6 Java DSL
ListedHashTree tree = testTree(b -> {
b.add(TestPlan.class, tp -> {
tp.setName("bookshelf-search");
tp.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName());
b.add(ThreadGroup.class, tg -> {
tg.setName("search");
tg.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
tg.setNumThreads(threads);
tg.setRampUp(1);
LoopController loops = new LoopController();
loops.setLoops(20);
loops.setContinueForever(false);
loops.setProperty(TestElement.GUI_CLASS, LoopControlPanel.class.getName());
tg.setSamplerController(loops);
b.add(HTTPSamplerProxy.class, http -> {
http.setName("search books");
http.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName());
http.setProtocol("http");
http.setDomain("localhost");
http.setPort(port);
http.setPath("/api/books");
http.setMethod("GET");
http.setFollowRedirects(true); // ticked by default in the GUI, false in code
http.setUseKeepAlive(true); // idem
http.addArgument("query", "dune");
});
});
});
});
// 3. Save it as a .jmx that the GUI and CLI can open
try (OutputStream out = new FileOutputStream("bookshelf-search.jmx")) {
SaveService.saveTree(tree, out);
}
// 4. Print the Kotlin DSL that the GUI "Copy Code" action would produce
DslPrinterTraverser dsl = new DslPrinterTraverser();
tree.traverse(dsl);
System.out.println(dsl);
// 5. Add a summariser and a JTL writer under the Test Plan, then run
Summariser summariser = new Summariser("summary");
ResultCollector jtl = new ResultCollector(summariser);
jtl.setFilename("bookshelf-search.jtl");
tree.add(tree.getArray()[0], jtl);
StandardJMeterEngine engine = new StandardJMeterEngine();
engine.configure(tree);
engine.runTest();
engine.awaitTermination(Duration.ofMinutes(5));
System.exit(0);
}
}
Compile and run it against an installed JMeter, with the jars in lib/ and lib/ext/ on the class path (5 threads,
port 8080 for the local Bookshelf API):
JMETER_HOME=/opt/apache-jmeter-5.6.3
CP="$JMETER_HOME/lib/*:$JMETER_HOME/lib/ext/*"
javac --release 21 -cp "$CP" -d out BookshelfSearchPlan.java
java -cp "out:$CP" BookshelfSearchPlan "$JMETER_HOME" 5 8080
The program prints the Kotlin DSL of the tree (abridged here) and then the summariser line; 5 threads times 20 loops give 100 samples:
org.apache.jmeter.testelement.TestPlan::class {
props {
it[name] = "bookshelf-search"
it[guiClass] = "org.apache.jmeter.control.gui.TestPlanGui"
}
org.apache.jmeter.threads.ThreadGroup::class {
props {
it[name] = "search"
it[guiClass] = "org.apache.jmeter.threads.gui.ThreadGroupGui"
it[numThreads] = 5
it[rampTime] = 1
it[mainController] = org.apache.jmeter.control.LoopController().apply {
props {
it[loops] = 20
it[continueForever] = false
...
summary = 100 in 00:00:01 = 114.4/s Avg: 3 Min: 1 Max: 75 Err: 0 (0.00%)
The summariser formats numbers with the JVM’s default locale; with a Spanish or German locale the rate reads
114,4/s. Add -Duser.language=en -Duser.country=US when a script parses the line.
The saved bookshelf-search.jmx is an ordinary plan; jmeter -n -t bookshelf-search.jmx -l results.jtl runs it with
the same result:
<?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="bookshelf-search"/>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="search">
<intProp name="ThreadGroup.num_threads">5</intProp>
<intProp name="ThreadGroup.ramp_time">1</intProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel">
<intProp name="LoopController.loops">20</intProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="search books">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="query" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">true</boolProp>
<stringProp name="Argument.name">query</stringProp>
<stringProp name="Argument.value">dune</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<intProp name="HTTPSampler.port">8080</intProp>
<stringProp name="HTTPSampler.path">/api/books</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
To build with Maven instead of a class path of jar files, JMeter publishes its modules to Maven Central. The example
needs the core and the HTTP module; it still reads jmeter.properties from an installed JMeter (or from a copy of that
file shipped with the tests):
<properties>
<maven.compiler.release>21</maven.compiler.release>
<jmeter.version>5.6.3</jmeter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>${jmeter.version}</version>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_http</artifactId>
<version>${jmeter.version}</version>
</dependency>
</dependencies>
Loading and running an existing .jmx from code
The other common use of the API is to run a GUI-built plan from a Java program or test harness, changing properties
first. This mirrors what jmeter -n does internally (in JMeter.runNonGui):
import java.io.File;
import java.time.Duration;
import org.apache.jmeter.JMeter;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.reporters.Summariser;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
public class RunExistingPlan {
public static void main(String[] args) throws Exception {
String jmeterHome = args[0];
File jmx = new File(args[1]); // e.g. bookshelf-load.jmx
JMeterUtils.setJMeterHome(jmeterHome);
JMeterUtils.loadJMeterProperties(jmeterHome + "/bin/jmeter.properties");
JMeterUtils.initLocale();
JMeterUtils.setProperty("threads", "2"); // same as -Jthreads=2
FileServer.getFileServer().setBaseForScript(jmx); // relative CSV paths resolve next to the .jmx
HashTree tree = SaveService.loadTree(jmx);
HashTree runnable = JMeter.convertSubTree(tree, true); // drop disabled elements, clone
Summariser summariser = new Summariser("summary");
ResultCollector jtl = new ResultCollector(summariser);
jtl.setFilename("existing-plan.jtl");
runnable.add(runnable.getArray()[0], jtl);
StandardJMeterEngine engine = new StandardJMeterEngine();
engine.configure(runnable);
engine.runTest();
engine.awaitTermination(Duration.ofMinutes(15));
System.exit(0);
}
}
With bookshelf-load.jmx reading ${__P(threads,1)}, the setProperty call has the same effect as -Jthreads=2.
JMeter.convertSubTree(tree, true) removes disabled elements, resolves Module Controllers and clones the tree, as the
CLI does; its Javadoc labels it "internal JMeter API", so pin the JMeter version when you depend on it. Plans that use
Module Controllers need more of the CLI’s preparation (it builds a non-GUI tree model first); for those, prefer
starting jmeter -n as a process.
Cost on the load generator
Building the tree in code costs nothing measurable; the engine, samplers and listeners are the same as in CLI mode, so the sizing advice of the section applies unchanged. Two differences come from where the engine runs:
-
It shares the JVM with your program. Heap, garbage collection and CPU are those of the Java process you started (or of the Maven Surefire fork for tests), not the
HEAPsettings ofbin/jmeter. Size that JVM explicitly (-Xms/-Xmxinjava, orargLinefor Surefire) for real load. -
Nothing is added for you. No summariser, no JTL and no dashboard unless the code adds them, and no shutdown port listener, so
bin/shutdown.shcannot stop a test started this way;engine.stopTest()from code can.
Pitfalls of the raw API
-
GUI defaults are not applied. The GUI fills new elements with defaults when you add them (5.6.3 moved this into
JMeterGUIComponent.assignDefaultValues, PR 6199). In code an unset boolean isfalse: anHTTPSamplerProxydoes not follow redirects and does not use keep-alive unless you set both, and aTestPlandoes not run tearDown thread groups after a shutdown (TestPlan.tearDown_on_shutdown) unless you set it. Converting the API-built plan withjmx2dsl(below) made this visible: before the two setters were added, the generated code contained.followRedirects(false)and.useKeepAlive(false). -
No
guiClass. Without it the.jmxruns in the CLI but the GUI cannot show proper editors for the elements. Set it per element (as above) or once withconfigureEach. -
A Thread Group without a controller starts, but every thread fails with
NoSuchElementException: Property ThreadGroup.main_controller is unsetand no sample is taken. The GUI creates the Loop Controller for you; code must callsetSamplerController. -
The documented API is small. The manual covers tree building only. Engine set-up,
SaveServiceand result collection come from the Javadoc and the source, and some of it is internal. -
Experimental status.
TreeBuilder,testTree,DslPrinterTraverserandStandardJMeterEngine.awaitTerminationcarry@API(status = EXPERIMENTAL, since = "5.6"); they were still experimental on the master branch on 2026-10-14. Signatures may change in a later release.
JMeter Java DSL (Abstracta)
What it is
The JMeter Java DSL (us.abstracta.jmeter:jmeter-java-dsl) is an open-source library by Abstracta, not part of
Apache JMeter. It describes itself as a "Simple JMeter performance tests Java API". A test is a JUnit (or any other
framework) test that builds a plan with static factory methods, runs it in an embedded JMeter engine, and gets back a
TestPlanStats object to assert on. It predates JMeter’s own DSL (first release 0.1 in August 2020, 1.0 in September
2022) and is independent of it.
| Item | Value (checked 2026-10-14) |
|---|---|
Current version |
2.3, released 2026-10-07 (Maven Central and GitHub release |
JMeter inside |
5.6.3 (the |
Java |
The library is compiled for Java 8. The |
Plugins it brings |
|
Licence |
Apache License 2.0 |
Optional modules, each with the same version: jmeter-java-dsl-dashboard (live charts), -graphql, -jdbc,
-websocket, -parallel, -wrapper (any JMeter element or GUI), -recorder and -cli (recorder and jmx2dsl),
-blazemeter, -octoperf, -azure (cloud engines), -elasticsearch-listener, -prometheus, -datadog, and
-bridge.
Setup
Maven, with JUnit 5 and AssertJ for the assertions:
<dependencies>
<dependency>
<groupId>us.abstracta.jmeter</groupId>
<artifactId>jmeter-java-dsl</artifactId>
<version>2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.6</version>
<scope>test</scope>
</dependency>
</dependencies>
Gradle (Kotlin script), as given by the guide; the exclusion avoids resolving JMeter’s bom artifact:
dependencies {
testImplementation("us.abstracta.jmeter:jmeter-java-dsl:2.3") {
exclude("org.apache.jmeter", "bom")
}
}
No JMeter installation is needed: JMeter’s jars come as dependencies, and the library prepares the JMeter home and properties it needs at run time.
First test: a verified smoke run
testPlan(...) takes the plan’s children, threadGroup(name, threads, iterations, children...) creates a standard
Thread Group, and httpSampler(label, url) an HTTP Request. run() blocks until the test ends and returns the
statistics. This test was run against a local stub that serves a static JSON file:
package bookshelf;
import static org.assertj.core.api.Assertions.assertThat;
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
import java.io.IOException;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
class SearchSmokeTest {
private static final String BASE = "http://localhost:" + System.getProperty("port", "8080");
@Test
void searchStaysFast() throws IOException {
TestPlanStats stats = testPlan(
threadGroup("search", 5, 20,
httpSampler("search books", BASE + "/api/books?query=dune")
.children(responseAssertion().containsSubstrings("Dune"))
),
jtlWriter("target/jtls"),
htmlReporter("target/reports")
).run();
assertThat(stats.overall().samplesCount()).isEqualTo(100);
assertThat(stats.overall().errorsCount()).isZero();
assertThat(stats.byLabel("search books").sampleTimePercentile99())
.isLessThan(Duration.ofMillis(500));
System.out.println("p99=" + stats.overall().sampleTimePercentile99()
+ " samples=" + stats.overall().samplesCount()
+ " rps=" + stats.overall().samples().perSecond());
}
}
mvn test -Dtest=SearchSmokeTest -Dport=8080
p99=PT0.05S samples=100 rps=416.6666666666667
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.209 s -- in bookshelf.SearchSmokeTest
[INFO] BUILD SUCCESS
target/jtls then holds one JTL file in the CSV format of the section (timeStamp,elapsed,label,responseCode,...),
and target/reports one dashboard folder. When the same test was first pointed at a port where another server
answered 404, the run failed on errorsCount() with "expected: 0L but was: 100L": HTTP errors become failed samples
and failed samples fail the JUnit test.
The Bookshelf journey in the DSL
The full user journey of the section (log in, search, view a book, create a loan, view the loan), with the same run
properties (threads, rampup, duration, host, port), the same CSV files and variables (username,
password, term, token, bookId, loanId), JSON Extractors with the TOKEN_NOT_FOUND style defaults, and the
same SLA properties (search_sla_ms, login_sla_ms). The files users.csv and search-terms.csv (placeholder
values only) sit in src/test/resources. This class compiles against 2.3; it needs the Bookshelf API to run:
package bookshelf;
import static org.assertj.core.api.Assertions.assertThat;
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
import java.io.IOException;
import java.time.Duration;
import org.apache.http.entity.ContentType;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
import us.abstracta.jmeter.javadsl.core.postprocessors.DslJsonExtractor.JsonQueryLanguage;
class BookshelfJourneyTest {
// Same run properties as the .jmx plan: -Dthreads=50 -Drampup=60 -Dduration=660
private static final int THREADS = Integer.getInteger("threads", 1);
private static final int RAMPUP = Integer.getInteger("rampup", 1);
private static final int DURATION = Integer.getInteger("duration", 30);
private static final String BASE_URL = "http://" + System.getProperty("host", "localhost")
+ ":" + System.getProperty("port", "8080");
@Test
void loanJourneyMeetsSla() throws IOException {
TestPlanStats stats = testPlan(
httpDefaults()
.url(BASE_URL)
.connectionTimeout(Duration.ofSeconds(5))
.responseTimeout(Duration.ofSeconds(30)),
csvDataSet(testResource("users.csv")),
csvDataSet(testResource("search-terms.csv")),
threadGroup("loan journey")
.rampToAndHold(THREADS, Duration.ofSeconds(RAMPUP),
Duration.ofSeconds(DURATION - RAMPUP))
.children(
transaction("01 login",
httpSampler("POST /api/auth/login", "/api/auth/login")
.post("{\"username\":\"${username}\",\"password\":\"${password}\"}",
ContentType.APPLICATION_JSON)
.children(
jsonExtractor("token", "$.token")
.queryLanguage(JsonQueryLanguage.JSON_PATH)
.defaultValue("TOKEN_NOT_FOUND"))),
uniformRandomTimer(Duration.ofSeconds(2), Duration.ofSeconds(4)),
transaction("02 search",
httpSampler("GET /api/books", "/api/books")
.param("query", "${term}")
.header("Authorization", "Bearer ${token}")
.children(
jsonExtractor("bookId", "$[0].id")
.queryLanguage(JsonQueryLanguage.JSON_PATH)
.defaultValue("BOOK_NOT_FOUND"))),
transaction("03 view book",
httpSampler("GET /api/books/{id}", "/api/books/${bookId}")
.header("Authorization", "Bearer ${token}")
.children(responseAssertion().containsSubstrings("\"id\""))),
transaction("04 create loan",
httpSampler("POST /api/loans", "/api/loans")
.header("Authorization", "Bearer ${token}")
.post("{\"bookId\":\"${bookId}\"}", ContentType.APPLICATION_JSON)
.children(
jsonExtractor("loanId", "$.id")
.queryLanguage(JsonQueryLanguage.JSON_PATH)
.defaultValue("LOAN_NOT_FOUND"))),
transaction("05 view loan",
httpSampler("GET /api/loans/{id}", "/api/loans/${loanId}")
.header("Authorization", "Bearer ${token}"))
),
jtlWriter("target/jtls", "bookshelf-journey.jtl"),
htmlReporter("target", "bookshelf-dashboard")
).run();
assertThat(stats.overall().errors().total()).isZero();
assertThat(stats.byLabel("02 search").sampleTime().perc95())
.isLessThan(Duration.ofMillis(Long.getLong("search_sla_ms", 800)));
assertThat(stats.byLabel("01 login").sampleTime().perc95())
.isLessThan(Duration.ofMillis(Long.getLong("login_sla_ms", 1000)));
}
}
Source: JMeter Java DSL user guide (sections CSV as input data, JSON extraction, Group requests, Emulate user delays, Timeouts, Generate HTML reports)
Run the smoke profile with the defaults and the load profile with system properties:
mvn test -Dtest=BookshelfJourneyTest # smoke: 1 thread, 30 s
mvn test -Dtest=BookshelfJourneyTest -Dthreads=50 -Drampup=60 -Dduration=660 # load
Worked numbers for the load profile. rampToAndHold(50, 60 s, 600 s) becomes a standard Thread Group with 50
threads, a 60 s ramp-up and a scheduler duration of 660 s (checked in the saved .jmx), the section’s load profile.
Threads start at 50 / 60 ≈ 0.83 per second. The uniformRandomTimer(2 s, 4 s) sits at thread-group level, so it
delays every sampler by 3 s on average; with a typical response time of 0.2 s each thread sends one request per
≈ 3.2 s, so 50 threads send ≈ 50 / 3.2 ≈ 15.6 requests per second at full load. Over the 600 s hold that is
≈ 9,400 requests, plus ≈ 470 during the ramp (half the rate for 60 s).
Points in this example that differ from a GUI plan:
-
Strings are JMeter expressions.
${token}and${term}are resolved by JMeter at run time, exactly as in a.jmx; Java only builds the strings. -
jsonExtractoruses JMESPath by default.queryLanguage(JsonQueryLanguage.JSON_PATH)switches it to the JSON Extractor and JSONPath used elsewhere in the section.jsonAssertionhas the same default. -
Transactions. The DSL’s
transactionwrites "Include duration of timer and pre-post processors" asfalseexplicitly and does not generate a parent sample unlessgenerateParentSample()is called, which fits the section’s convention of always setting the property. -
uniformRandomTimer(min, max)is not the GUI’s field pair. The guide notes that the generated Uniform Random Timer gets Constant Delay Offset = min and Random Delay Maximum = max − min. -
Cookies and caching are on by default. The DSL adds an HTTP Cookie Manager and an HTTP Cache Manager to every plan;
httpCookies().disable()andhttpCache().disable()remove them. -
CSV sharing.
csvDataSetshares one file across all threads by default and sets Allow quoted data to true;sharedIn(...)changes the sharing mode.
How DSL builders map to JMeter elements
| DSL builder | JMeter element it creates | Notes |
|---|---|---|
|
Test Plan |
Also |
|
Thread Group |
Iterations or a scheduler duration. |
|
Thread Group, or Ultimate Thread Group (plugin) |
A single ramp and hold stays a standard Thread Group; multi-stage profiles become an Ultimate Thread Group. |
|
Concurrency Thread Group + Throughput Shaping Timer (plugins) |
See below. |
|
setUp / tearDown Thread Group |
|
|
HTTP Request, HTTP Request Defaults, HTTP Header Manager |
|
|
Transaction Controller |
|
|
CSV Data Set Config |
|
|
Response Assertion, JSON JMESPath or JSON Assertion |
|
|
JSON JMESPath / JSON Extractor, Regular Expression Extractor, Boundary Extractor |
|
|
Constant Timer, Uniform Random Timer, Constant Throughput Timer, Flow Control Action pause |
|
|
JSR223 elements (Groovy) |
Also accept Java lambdas, which
cannot be exported to |
|
|
|
|
HTML dashboard generated after the run |
|
|
Backend Listener (InfluxDB or Graphite client) |
Source: JMeter Java DSL user guide and the buildTestElement methods of the 2.3 sources
Load shaping: thread ramps and rpsThreadGroup
The default threadGroup() chains stages: rampTo(threads, duration), holdFor(duration),
rampToAndHold(threads, ramp, hold) and holdIterating(n). showTimeline() opens a chart of the planned threads.
rpsThreadGroup() targets requests per second instead of threads. It adds and removes threads and inserts delays
to match a rate profile, using the Concurrency Thread Group with the Throughput Shaping Timer’s feedback function
internally (the plugin elements the section describes in
Custom Thread Groups & Load Shaping).
.counting(RpsThreadGroup.EventType.ITERATIONS) targets iterations per second instead.
rpsThreadGroup("search arrivals")
.maxThreads(20)
.rampToAndHold(15.6, Duration.ofSeconds(60), Duration.ofSeconds(600))
.children(
httpSampler("GET /api/books", "/api/books").param("query", "${term}"))
Worked numbers. The guide’s sizing rule is maxThreads ≈ T × R, with T the highest rate and R the highest
expected response time in seconds. For 15.6 requests per second and a pessimistic 0.5 s response time, T × R ≈ 7.8, so
at least 8 threads; maxThreads(20) leaves headroom for slow periods. Without maxThreads the group creates as many
threads as needed, which the guide warns can exhaust the load generator. Do not add think-time timers here: the shaping
timer already inserts the pauses, and extra timers make each thread slower, which the group compensates with more
threads. The saved .jmx showed the ramp starting at 1 request per second, not 0, and the guide advises against rates
below 1 per second.
Saved as .jmx, an rpsThreadGroup plan uses DSL subclasses of the plugin elements (for example
RpsThreadGroup$ContractComplyingConcurrencyThreadGroup), so the GUI can open it only with the DSL jar copied to
JMeter’s lib folder, as the guide’s "Save as JMX" tip describes. For a plan that must be maintained in the GUI,
use the Open Model Thread Group or the plugin elements directly (see
Open Model & Throughput Control).
Results: jtlWriter, htmlReporter and influxDbListener
| Listener | Options (2.3) |
|---|---|
|
Writes a CSV JTL; without a name the file is called after the date and
a UUID. |
|
Generates JMeter’s HTML dashboard after the run; a fixed name must
be unique or generation fails. |
|
JMeter’s InfluxDB Backend Listener client. Options: |
|
Graphite Backend Listener. |
|
GUI windows during the run, for debugging on a desktop only
( |
testPlan(
threadGroup("search", 5, Duration.ofMinutes(2),
httpSampler("GET /api/books", "http://localhost:8080/api/books").param("query", "dune")),
influxDbListener("http://localhost:8086/api/v2/write?org=bookshelf&bucket=jmeter")
.token(System.getenv("INFLUXDB_TOKEN"))
.application("bookshelf")
.tag("profile", "smoke")
).run();
The token comes from the environment, never from source code. The JMeter side of InfluxDB and Grafana is described in Real-time Monitoring with Backend Listener, and the dashboard in HTML Dashboard Report.
Assertions on TestPlanStats with JUnit 5
run() returns TestPlanStats. It aggregates every sample of the run, overall and by label (sampler and transaction
names), and is what makes the DSL attractive in CI: the build fails on an ordinary assertion.
| Method (2.3) | Returns |
|---|---|
|
A |
|
Run time as a |
|
Totals (shortcuts for |
|
Rates over the run. |
|
Response-time statistics as
|
|
Byte totals and rates. |
|
First and last sample time. |
Assertions are evaluated after the run, so a test that is far off target still runs to the end. The DSL’s
autoStop(...) listener stops the run early when a condition on these metrics is met and fails the test. Error rates
are not a field; compute them as errors().total() divided by samples().total().
Reviewing in the GUI: showInGui and showTimeline
testPlan(...).showInGui() opens the JMeter GUI with the plan loaded, so a GUI user can check what the code builds,
add a View Results Tree and run it interactively. Any DSL element supports it, not only the plan. showTimeline() on
a thread group draws the planned threads or rate over time. Both need a desktop session; remove them from tests that
run in CI.
testPlan(
threadGroup("search", 2, 10,
httpSampler("GET /api/books", "http://localhost:8080/api/books").param("query", "dune"))
).showInGui();
JMX round trips: saveAsJmx, fromJmx and jmx2dsl
-
saveAsJmx(path)writes the plan as a.jmx. The DSL saved the smoke plan with JMeter 5.6.3’s own format, including its automatic Cookie and Cache Managers, andjmeter -n -t dsl-search.jmxran it without any extra jar. Plans with DSL-specific elements (rpsThreadGroup, Java lambdas) need the DSL jar in JMeter’slib, and lambdas do not work in the GUI at all. -
DslTestPlan.fromJmx(path).run()runs an existing.jmxin the embedded engine and returnsTestPlanStats, so a GUI-built plan can get JUnit assertions without being rewritten. -
jmx2dslconverts a.jmxinto DSL code, throughjmdsl.jarfrom the GitHub release page or JBang:
java -jar jmdsl.jar jmx2dsl bookshelf-search.jmx
# or
jbang us.abstracta.jmeter:jmeter-java-dsl-cli:2.3 jmx2dsl bookshelf-search.jmx
Applied to the bookshelf-search.jmx produced with JMeter’s API earlier on this page, the generated test contains:
TestPlanStats stats = testPlan()
.tearDownOnlyAfterMainThreadsDone()
.children(
httpCache()
.disable(),
httpCookies()
.disable(),
threadGroup("search", 5, 20,
httpSampler("search books", "http://localhost:8080/api/books")
.param("query", "dune")
)
).run();
assertThat(stats.overall().errorsCount()).isEqualTo(0);
Source: DSL code generation from JMX file (JMeter Java DSL user guide) and JMX support (JMeter Java DSL user guide)
The conversion is a good check of what a plan really contains: it disabled the DSL’s automatic cookie and cache
managers because the original plan has none, and added tearDownOnlyAfterMainThreadsDone() because the API-built
Test Plan left tearDown_on_shutdown at false. The guide recommends reviewing generated code, adding assertions and
running it with one thread first.
Running at scale
run() uses the EmbeddedJmeterEngine in the test’s JVM. runIn(engine) selects another engine:
EmbeddedJmeterEngine with prop(name, value) or propertiesFile(file) (JMeter properties are only supported by this
engine), DistributedJmeterEngine("host1", "host2") for JMeter remote testing (the servers must run the same JMeter
version as the DSL, 5.6.3, and rmi_keystore.jks must be in the working directory), and the BlazeMeter, OctoPerf and
Azure Load Testing engines from their modules. The guide itself recommends the managed services over a self-run RMI
cluster. Remote testing in plain JMeter is described in
Distributed Testing.
Cost on the load generator
The embedded engine is real JMeter, so samplers, timers and listeners cost the same as in CLI mode. What changes:
-
The test JVM is the load generator. Maven Surefire forks a JVM with its own defaults; give it a heap that fits the run (for example
<argLine>-Xms1g -Xmx1g</argLine>) and keep the forked JVM to one performance test at a time. Do not run load tests in parallel with the unit test suite on the same machine. -
htmlReporterruns after the test and parses the whole JTL; for long soak runs it takes noticeable time and memory at the end. -
jtlWriter().withAllFields()and the GUI visualizers add per-sample work; keep them for debugging. -
rpsThreadGroupwithoutmaxThreadscan create more threads than the machine can hold.
What the DSL does not cover
-
No gRPC. Neither the guide nor the module list contains gRPC support; an open feature request in the project’s GitHub issues (number 265) asks for HTTP/2 and gRPC. For gRPC use the JMeter gRPC Request plugin in a
.jmxplan as described in gRPC Load Testing, or try to wrap that plugin’s sampler with the DSL’sjmeter-java-dsl-wrappermodule (not verified for this plugin). -
No HTTP/2. The
httpSampleruses JMeter’s HTTP Request (HttpClient4 or the Java implementation). -
Not every JMeter element has a builder. The
-wrappermodule and custom builder classes (subclasses ofBaseSamplerand similar) cover the gaps, as the guide’s "Custom or yet not supported test elements" section shows. -
Not a browser. Like JMeter, it does not run JavaScript. The guide’s Selenium section runs an existing Selenium script inside a
jsr223Samplerto time a few real-browser users next to the HTTP load; the browser itself is not load-tested by JMeter. -
JMeter version lag. Each DSL release pins one JMeter version. JMeter 6.0 support depends on a future DSL release.
Choosing between .jmx and code
| Criterion | .jmx built in the GUI |
JMeter 5.6 API / DSL | JMeter Java DSL (Abstracta) |
|---|---|---|---|
Who maintains it |
Testers who know the GUI |
Java or Kotlin developers who know JMeter’s classes |
Java developers; JMeter knowledge helps but is not required |
Status |
Stable, the reference format |
Experimental ( |
Third-party, active, release 2.3 |
Coverage |
Every core element and every plugin |
Every element (any |
Most common elements; gaps through wrappers; no gRPC |
Review and merge |
Noisy XML diffs |
Readable code |
Readable code |
Pass or fail in CI |
Post-process JTL or |
Code you write around the engine |
JUnit / AssertJ on
|
Run in |
GUI, CLI, jmeter-maven-plugin, distributed RMI |
Your JVM, or save |
Test JVM, RMI cluster, BlazeMeter, OctoPerf, Azure |
Converts to the others |
Copy Code (Kotlin), |
|
|
Practical rules for Bookshelf-style projects:
-
Keep
.jmxwhen testers maintain the plan in the GUI, when it relies on plugins (gRPC, Ultimate Thread Group edited by hand) or when the run is a large distributed test driven by the CLI or the jmeter-maven-plugin (CI/CD with Maven, Docker & GitHub Actions). -
Use the JMeter Java DSL for performance checks that developers own and that run with every build: a smoke or short load test of a few endpoints with SLA assertions, next to the service’s other tests.
fromJmxlets such a build also run GUI-built plans. -
Use JMeter’s own API when a tool must generate or transform plans (for example one sampler per OpenAPI operation), or when a Java program must load, patch and run existing
.jmxfiles. Accept that it is experimental and pin the JMeter version. -
Do not maintain the same plan twice. Choose a source of truth; generate the other form (Copy Code,
jmx2dsl,saveAsJmx) only to inspect or migrate.
Pitfalls checklist
-
An API-built
HTTPSamplerProxydoes not follow redirects or use keep-alive unless both are set; an API-built Test Plan does not run tearDown groups after shutdown. -
An API-built plan without
guiClassvalues runs in the CLI but does not open properly in the GUI. -
HashTreeinstead ofListedHashTreecan reorder samplers. -
No
SummariserorResultCollector, no output: code must add them. -
Copy Code and
jmx2dslcopy credentials stored in the plan into source code. -
DSL
jsonExtractorandjsonAssertiondefault to JMESPath, not JSONPath. -
DSL plans get a Cookie Manager and a Cache Manager unless disabled.
-
uniformRandomTimer(min, max)means minimum and maximum, not the GUI’s offset and random range. -
rpsThreadGroupwithoutmaxThreads, or with rates below 1 per second, misbehaves; plans using it need the DSL jar to open in the GUI. -
A unique
jtlWriterfile name andhtmlReporterdirectory per run, or results are appended or generation fails. -
showInGui()and visualizers in a CI test block or fail without a desktop. -
Performance tests in the same Surefire run as unit tests distort both; run them in their own profile or module.
-
Summariser output uses the JVM locale for decimals.
What changed: from the books to 5.6.3 and 6.0
-
The books (JMeter 2.11 to 3.0, 2014 to 2017) build plans in the GUI only and do not cover a programmatic API or any DSL; their methodology for workload models and acceptance criteria applies to plans written in code unchanged.
-
2020 to 2022: the JMeter Java DSL appears (0.1 in August 2020, 1.0 in September 2022) as a third-party project.
-
JMeter 5.6 (June 2023): experimental Kotlin and Java DSL (
TreeBuilder,testTree, PR 678), the Copy Code action, and the property schema API used by the generated code (TestElementSchema,getProps(), PR 5944); theTestElementinterface moved to Kotlin (PR 5959). -
JMeter 5.6.3 (January 2024): improved API for creating elements programmatically in GUI components (
makeTestElement,assignDefaultValues, PR 6199). The DSL classes remain experimental. -
JMeter Java DSL 2.0 (September 2025) moves to JMeter 5.6.3; 2.3 (October 2026) is the current release.
-
JMeter 6.0 (planned per
changes.xmlon master): Java 17 or later and Kotlin 1.9 or later required (PRs 6220 and 6550);TreeBuilderandtestTreewere still marked experimental on master on 2026-10-14. Planned behaviour changes also reach code-built plans, for example the Cookie Manager no longer sending expired cookies (issue 6428), 307/308 redirects keeping their method (issue 6080) and the MongoDB elements being removed (PR 6573). Re-check after 6.0 is released; the JMeter Java DSL will need a release built on 6.0.
Related pages
-
Test Plan Elements, Execution Order & Scoping: the tree that both APIs build.
-
Properties, Variables & Functions: the run properties shared by
.jmxand code. -
Running Tests in CLI Mode and Listeners & Results Files: what
jmeter -nadds around the engine. -
Custom Thread Groups & Load Shaping: the plugin elements behind
rpsThreadGroupand multi-stagethreadGroup. -
gRPC Load Testing: gRPC, which neither DSL covers.
-
CI/CD with Maven, Docker & GitHub Actions: running
.jmxplans in pipelines.
References
-
Apache JMeter source (GitHub), tag
rel/v5.6.3:TreeBuilder.kt,TreeBuilders.kt,Action.kt,CopyCodeAction.kt,DslPrinterTraverser.kt,MenuFactory.java,JMeter.java,StandardJMeterEngine.java,TestPlanSchema.kt,HTTPSamplerBaseSchema.kt;xdocs/changes.xmlon master for the planned 6.0 changes -
abstracta/jmeter-java-dsl (GitHub), its releases and the 2.3 sources
-
Concurrency Thread Group, Ultimate Thread Group and Throughput Shaping Timer (JMeter Plugins)