Extending JMeter
|
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.
JMeter was designed so that almost everything in it can be replaced or extended by a jar dropped into lib/ext:
samplers, config elements, timers, assertions, listeners, functions, Backend Listener clients and the charts of the
HTML dashboard. This page shows how to write each of the extension points a load-testing team is likely to need, how to
build them with Maven against the JMeter 5.6.3 jars, how JMeter 5.6 finds them, how to deploy them on every load
generator and how to publish them so that others can install them with the JMeter Plugins Manager.
Versions this page was written against, re-checked on 2026-10-14:
-
Apache JMeter 5.6.3, the latest release. It runs on Java 8 or later; Java 17 or later is recommended and the examples are compiled with
release 17and run with a current JDK. Its Javadoc is published at https://jmeter.apache.org/api/index.html and matches 5.6.3 (it already documents the 5.6.3 methodJMeterGUIComponent.makeTestElement). -
JMeter 6.0 is not released. The changes planned for it come from
xdocs/changes.xmlon theapache/jmetermaster branch and are described as planned, not as shipped behaviour. The ones that matter for extensions are summarised in From the books to 5.6 to 6.0. -
Maven 3.9 and the JMeter artifacts
org.apache.jmeter:ApacheJMeter_core,ApacheJMeter_javaandApacheJMeter_components5.6.3 from Maven Central. -
JMeter Plugins Manager 2.0 and the jmeter-maven-plugin 3.8.0, for installing the result.
Every API on this page comes from the JMeter user manual chapter
How to write a plugin for JMeter, the Javadoc and the 5.6.3
source code (tag rel/v5.6.3 of apache/jmeter); where the manual is silent the page says that the behaviour was
read from the source. The example project on this page, bookshelf-jmeter-ext, was compiled against the 5.6.3
artifacts and run in CLI mode on a JMeter 5.6.3 installation against a local stand-in for the Bookshelf API; the
outputs shown are from those runs.
Do you need Java at all? Many one-off needs are met faster without a plugin: a JSR223 Sampler or PostProcessor in
Groovy (see Scripting with JSR223 & Groovy), the
__groovy function, or an existing plugin from the catalogue (see
Plugins & Plugins Manager). Write a Java extension when the code
is reused across plans and teams, needs a proper form in the GUI, must be unit tested and versioned like any other
library, or must be as cheap as possible on the load generator.
Choosing an extension point
| You need | Extension point | Base class to extend | Found by |
|---|---|---|---|
To call a Java client library (vendor SDK, proprietary protocol) with parameters, without writing a GUI |
A Java Request client |
|
|
A reusable sampler (or config element, timer, assertion, pre/post-processor) with its own form in the GUI, generated from bean properties |
A TestBean |
|
Class scanning |
The same, with a hand-made Swing form |
A test element plus a GUI class |
|
Class scanning |
A new |
A function |
|
|
To send results somewhere while the test runs (a database, a queue, an observability backend) |
A Backend Listener client |
|
|
A new chart in the HTML dashboard report |
A graph consumer |
|
Class name in |
GUI visualizers (AbstractVisualizer, GraphListener) are deliberately left out. The tutorial itself now opens those
sections by saying that load testing in GUI mode is a bad practice, that such plugins should not be developed, and that
the web report and real-time results through a BackendListenerClient are the up-to-date alternatives; this page
covers exactly those two.
The extension points at a glance
The class diagram shows the types a plugin author touches. Classes in the com.example.bookshelf.jmeter packages are
the examples built on this page; everything else is JMeter 5.6.3 API.
Three of the interfaces, JavaSamplerClient, Function and BackendListenerClient, carry the 5.6 annotation
@org.apache.jorphan.reflect.JMeterService, which marks them as loadable through Java’s ServiceLoader (see
Registration: ServiceLoader and class scanning). Samplers, TestBeans and GUI classes are still found by scanning
the jars.
The project: a Maven build against JMeter 5.6.3
Which artifact holds which API
JMeter publishes each of its jars to Maven Central under the group org.apache.jmeter, with the same names as the
files in lib/ext. A plugin compiles against the ones that hold the APIs it uses:
| Artifact | APIs used on this page |
|---|---|
|
|
|
|
|
|
|
Only if the plugin builds on a bundled function or protocol, for example by extending an HTTP sampler. |
The pom.xml
All JMeter artifacts use provided scope: they are already in every JMeter installation, so the plugin jar must not
bundle them (a second copy of JMeter classes in lib/ext causes class-loading conflicts). The manifest entry is
explained in Registration: ServiceLoader and class scanning.
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.bookshelf</groupId>
<artifactId>bookshelf-jmeter-ext</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<jmeter.version>5.6.3</jmeter.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Already in JMeter's lib/ext: compile against them, never package them -->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>${jmeter.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_java</artifactId>
<version>${jmeter.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_components</artifactId>
<version>${jmeter.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifestEntries>
<JMeter-Skip-Class-Scanning>true</JMeter-Skip-Class-Scanning>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Choosing the Java release. The bytecode level of the plugin sets the oldest Java that can load it. JMeter 5.6.3
itself still runs on Java 8, so a plugin meant for every 5.6.3 user would have to be compiled with release 8.
release 17 matches the recommendation for 5.6.3 and the planned minimum of 6.0; a JMeter started on an older JVM
then fails to load the class with an UnsupportedClassVersionError. The examples also use java.net.http, which needs
Java 11.
Own dependencies. Libraries the plugin needs and JMeter does not ship use the default compile scope and are
deployed next to it in lib (or shaded into the plugin jar with relocated packages). Never shade a library that JMeter
already has in lib (Jackson, Commons, Log4j, Groovy): two versions on one class path fail in ways that depend on load
order.
Layout and build
bookshelf-jmeter-ext/
├── pom.xml
└── src/main/
├── java/com/example/bookshelf/jmeter/
│ ├── BookSearchClient.java Java Request client
│ ├── sampler/BookLookupSampler.java TestBean sampler
│ ├── sampler/BookLookupSamplerBeanInfo.java its GUI description
│ ├── functions/RandomIsbnFunction.java ${__randomIsbn(...)}
│ ├── backend/SlaBreachBackendListenerClient.java Backend Listener client
│ └── report/SlowSamplesPerSecondGraphConsumer.java dashboard graph
└── resources/
├── com/example/bookshelf/jmeter/sampler/BookLookupSamplerResources.properties
└── META-INF/services/
├── org.apache.jmeter.protocol.java.sampler.JavaSamplerClient
├── org.apache.jmeter.functions.Function
└── org.apache.jmeter.visualizers.backend.BackendListenerClient
mvn -B clean package
[INFO] Building bookshelf-jmeter-ext 1.0.0
[INFO] --- resources:3.4.0:resources (default-resources) @ bookshelf-jmeter-ext ---
[INFO] Copying 4 resources from src/main/resources to target/classes
[INFO] --- compiler:3.15.0:compile (default-compile) @ bookshelf-jmeter-ext ---
[INFO] Compiling 6 source files with javac [debug release 17] to target/classes
[INFO] --- jar:3.4.2:jar (default-jar) @ bookshelf-jmeter-ext ---
[INFO] Building jar: ./target/bookshelf-jmeter-ext-1.0.0.jar
[INFO] BUILD SUCCESS
The jar is about 16 KB: six classes, one resource bundle and three service files, with no JMeter class inside.
JMeter itself is built with Gradle (./gradlew runGui, ./gradlew createDist), which the tutorial describes for
contributors to JMeter. A plugin does not need JMeter’s build: it only compiles against the published jars, so Maven or
Gradle both work. The official Maven artifacts are the same for both.
Java Request clients
The Java Request sampler and the BookSearchClient example are described field by field on
Other Samplers; this section adds what a plugin author needs on
top of that. The example client in that section is the first class of this project, unchanged.
Life cycle
| Method | When JMeter calls it |
|---|---|
|
When the class is selected in the GUI and when JMeter needs to know the parameters; the names and default values returned fill the parameter table. JMeter may create extra instances just to call it. |
|
Once per thread, on that thread’s own instance, when the test starts. Read and parse parameters and build clients
here, not in |
|
Once per iteration. Must return a |
|
Once per thread at the end of the test, only if the class overrides it: the 5.6.3 |
JavaSamplerContext gives access to the parameters and to JMeter’s state:
| Method | Returns |
|---|---|
|
The parameter as a string, after functions and variables were evaluated for this sample. |
|
The parameter parsed as a number; the
one-argument forms throw |
|
Whether a parameter exists; all names. |
|
The thread’s context, its variables
( |
Registration
The Classname drop-down of the Java Request lists the JavaSamplerClient implementations found through the
ServiceLoader and by scanning the jars. The service file of this project is one line:
# src/main/resources/META-INF/services/org.apache.jmeter.protocol.java.sampler.JavaSamplerClient
com.example.bookshelf.jmeter.BookSearchClient
A Java Request has no form beyond the parameter table, no field validation and no menu entry of its own. When the element is used by several teams, or users keep getting parameter names wrong, a TestBean is the next step.
Custom samplers as TestBeans
The TestElement contract
Every element in a test plan is two objects: a test element (the model, which is saved in the .jmx and runs) and a
GUI component (the form in the tree). The tutorial insists on a strict split: the GUI class is stateless and never
keeps a reference to the element, because JMeter reuses one GUI instance for every element of that type. The GUI
copies values into the element in modifyTestElement and back into the form in configure.
The TestBean framework removes the GUI class altogether. A class that implements the marker interface
org.apache.jmeter.testbeans.TestBean gets a generic form, TestBeanGUI, built by introspection from its JavaBean
properties (public getter and setter pairs). The tutorial’s recipe is three files in the same package:
| File | Role |
|---|---|
|
The element. Implements |
|
Extends |
|
Resource bundle with the display names and tooltips, loaded by |
How a TestBean runs. The tutorial notes that JMeter clones the element when the test starts, so every thread gets
its own instance. Before each sample, JMeterThread calls TestBeanHelper.prepare(sampler), which evaluates the
element’s properties (functions and variables included) and copies the results into the bean’s fields through the
setters. Fields therefore hold the values for this sample of this thread, and ${bookId} in a field follows the
thread’s variable. JMeter 5.6 cached the bean property metadata in TestBeanHelper to make this cheaper (PR 5920).
Menu category. TestBeanGUI decides the menu from the interfaces the element implements: a Sampler appears
under Add → Sampler, a ConfigElement under Add → Config Element, and so on. The menu label is the bundle’s
displayName.
Example: a Bookshelf book lookup sampler
The sampler sends GET /api/books/{id} to the Bookshelf API with the JDK HTTP client and marks the sample
successful when the status equals the expected one. It stands in for any client library that has no JMeter sampler:
package com.example.bookshelf.jmeter.sampler;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testbeans.TestBean;
/** GET /api/books/{id} as a sampler with its own GUI form. */
public class BookLookupSampler extends AbstractSampler implements TestBean {
private static final long serialVersionUID = 1L;
// HttpClient is thread-safe: one instance shared by every thread
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
// TestBean properties: JMeter copies the element's properties into these fields
private String baseUrl;
private String bookId;
private int timeoutMs;
private String expectedStatus;
@Override
public SampleResult sample(Entry entry) {
SampleResult result = new SampleResult();
result.setSampleLabel(getName());
String url = baseUrl + "/api/books/" + bookId;
result.setSamplerData("GET " + url);
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofMillis(timeoutMs))
.GET()
.build();
result.sampleStart();
try {
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
result.sampleEnd();
String code = String.valueOf(response.statusCode());
result.setResponseCode(code);
result.setResponseData(response.body(), StandardCharsets.UTF_8.name());
result.setDataType(SampleResult.TEXT);
result.setSuccessful(code.equals(expectedStatus));
result.setResponseMessage(result.isSuccessful() ? "OK" : "Expected " + expectedStatus);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
result.sampleEnd();
result.setSuccessful(false);
result.setResponseMessage(e.toString());
} catch (Exception e) {
result.sampleEnd();
result.setResponseCode("500");
result.setResponseMessage(e.toString());
result.setSuccessful(false);
}
return result;
}
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public String getBookId() { return bookId; }
public void setBookId(String bookId) { this.bookId = bookId; }
public int getTimeoutMs() { return timeoutMs; }
public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
public String getExpectedStatus() { return expectedStatus; }
public void setExpectedStatus(String expectedStatus) { this.expectedStatus = expectedStatus; }
}
The bean info groups the four properties into two panels and sets defaults; expectedStatus becomes a drop-down that
only accepts the listed values:
package com.example.bookshelf.jmeter.sampler;
import java.beans.PropertyDescriptor;
import org.apache.jmeter.testbeans.BeanInfoSupport;
public class BookLookupSamplerBeanInfo extends BeanInfoSupport {
public BookLookupSamplerBeanInfo() {
super(BookLookupSampler.class);
createPropertyGroup("server", new String[] {"baseUrl", "timeoutMs"});
createPropertyGroup("request", new String[] {"bookId", "expectedStatus"});
PropertyDescriptor p = property("baseUrl");
p.setValue(NOT_UNDEFINED, Boolean.TRUE);
p.setValue(DEFAULT, "http://${__P(host,localhost)}:${__P(port,8080)}");
p = property("timeoutMs");
p.setValue(NOT_UNDEFINED, Boolean.TRUE);
p.setValue(DEFAULT, 5000);
p = property("bookId");
p.setValue(NOT_UNDEFINED, Boolean.TRUE);
p.setValue(DEFAULT, "${bookId}");
p = property("expectedStatus");
p.setValue(NOT_UNDEFINED, Boolean.TRUE);
p.setValue(DEFAULT, "200");
p.setValue(NOT_OTHER, Boolean.TRUE);
p.setValue(NOT_EXPRESSION, Boolean.TRUE);
p.setValue(TAGS, new String[] {"200", "404"});
}
}
And the resource bundle, in src/main/resources under the same package path:
displayName=Bookshelf Book Lookup
server.displayName=Server
request.displayName=Request
baseUrl.displayName=Base URL
baseUrl.shortDescription=Scheme, host and port of the Bookshelf API, without a trailing slash
timeoutMs.displayName=Timeout (ms)
timeoutMs.shortDescription=Request timeout in milliseconds
bookId.displayName=Book id
bookId.shortDescription=Id of the book to fetch, usually ${bookId}
expectedStatus.displayName=Expected status
expectedStatus.shortDescription=HTTP status that marks the sample as successful
Property attributes
BeanInfoSupport exposes these attribute names as constants, to be set with property(name).setValue(...). The
meanings come from the tutorial and the Javadoc of BeanInfoSupport and GenericTestBeanCustomizer:
| Constant | Type | Meaning |
|---|---|---|
|
Boolean |
The property may not be left null (required). A |
|
Object |
Initial value in the form. Must be non-null if |
|
Boolean |
The value is a constant: it is not parsed for functions or |
|
Boolean |
Not a free-form field: only values from |
|
String[] |
Predefined values; JMeter shows a drop-down. |
|
Boolean |
A multi-line text editor instead of a one-line field. |
|
Boolean |
The value is saved in the |
|
String |
Language of a text-editor field; the Javadoc describes it only as "TextEditor property". |
|
Integer |
Position within the group; smaller is higher. Properties with equal order are sorted alphabetically.
|
A custom editor replaces the default one: property("dataFile").setPropertyEditorClass(FileEditor.class) gives a
text field with a Browse button, as in the tutorial’s CSV example. GenericTestBeanCustomizer registers JMeter’s own editors for the Long, Integer
and Boolean wrapper types; the example’s String and int properties were set correctly at run time.
The resource bundle keys are:
| Key | Used for |
|---|---|
|
Name of the element in the Add menu and the default name in the tree. |
|
Title of each group created with |
|
Label of the field. |
|
Tooltip of the field. |
When the bundle is missing, BeanInfoSupport logs Localized strings not available for bean ... and the form shows
raw property names.
Using it in the Bookshelf plan
After deployment (see Deploying to lib/ext) the element appears under
Add → Sampler → Bookshelf Book Lookup, with a Server group (Base URL, Timeout (ms)) and a Request group (Book id,
Expected status). Saved, it looks like this; the element tag and testclass are the bean’s class name and
guiclass is always TestBeanGUI:
<com.example.bookshelf.jmeter.sampler.BookLookupSampler guiclass="TestBeanGUI" testclass="com.example.bookshelf.jmeter.sampler.BookLookupSampler" testname="GET /api/books/{id} (TestBean)">
<stringProp name="baseUrl">http://${__P(host,localhost)}:${__P(port,8080)}</stringProp>
<stringProp name="bookId">${bookId}</stringProp>
<intProp name="timeoutMs">5000</intProp>
<stringProp name="expectedStatus">200</stringProp>
</com.example.bookshelf.jmeter.sampler.BookLookupSampler>
<hashTree/>
Like any plugin element, this tag makes the .jmx unloadable on a JMeter without the jar, which is what lets the
Plugins Manager’s install-for-jmx find the plugin when it is published (see Publishing to the Plugins Manager repository).
Hand-written GUIs
When a generated form is not enough (tables, tabs, dependent fields), write the element without TestBean and pair it
with a GUI class. The tutorial lists the base classes to extend: AbstractSamplerGui, AbstractAssertionGui,
AbstractConfigGui, AbstractControllerGui, AbstractPostProcessorGui, AbstractPreProcessorGui,
AbstractVisualizer and AbstractTimerGui. The contract, from the tutorial and the 5.6.3 JMeterGUIComponent
Javadoc:
| Method | What to do |
|---|---|
|
|
|
New in 5.6.3: return |
|
Copy the element’s properties into the Swing fields. Call |
|
The reverse: call |
|
Reset controls that do not map to properties (tabs, sliders). |
Layout |
Call |
JMeter 5.6 also added TestElementSchema (PR 5944) to declare an element’s properties once and bind form fields to
them automatically; the tutorial points to ThreadGroupSchema as the example. The API is marked experimental in 5.6.3.
Complete, compilable examples of both styles ship in the JMeter source under src/examples:
ExampleSampler/ExampleSamplerGui (hand-written GUI) and Example1 to Example3 (TestBeans).
Custom functions
The Function contract
A function is any class implementing org.apache.jmeter.functions.Function, normally through AbstractFunction:
| Method | Role |
|---|---|
|
The name used in plans. The Javadoc’s convention is to start it with two underscores, as in |
|
One description per parameter, shown by the Function Helper dialog (Tools → Function Helper Dialog). Not optional: return a list with one (possibly blank) string per parameter. |
|
Called when the plan is compiled, with the raw parameters. Validate the count here
( |
|
Called every time the field is evaluated. Evaluate each parameter with |
Threading. JMeter registers each function class once by its reference key and creates a new instance for each
place the function appears in the plan; that instance is then shared by every thread that evaluates the field. The
Function Javadoc states it plainly: execute "must be thread-safe - multiple threads will be using the same
object". Keep state in local variables or in thread-safe structures, and use ThreadLocalRandom rather than a shared
Random.
No caching since 5.6. FunctionProperty no longer caches the value per iteration (PR 5788), so execute runs each
time the field is read. The old behaviour can be restored temporarily with the function.cache.per.iteration
property. Keep execute cheap.
Example: __randomIsbn
The Bookshelf catalogue needs syntactically valid ISBN-13 values to create books and to probe searches for unknown
books. ${__randomIsbn(979,isbn)} returns a random ISBN-13 with the given prefix and a correct check digit, and stores
it in the variable isbn:
package com.example.bookshelf.jmeter.functions;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.jmeter.engine.util.CompoundVariable;
import org.apache.jmeter.functions.AbstractFunction;
import org.apache.jmeter.functions.InvalidVariableException;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.samplers.Sampler;
/**
* ${__randomIsbn(prefix,varName)}: a random, valid ISBN-13 such as 9781234567897.
*/
public class RandomIsbnFunction extends AbstractFunction {
private static final String KEY = "__randomIsbn";
private static final List<String> DESC = List.of(
"Prefix, 978 or 979 (optional, default 978)",
"Name of variable in which to store the result (optional)");
private CompoundVariable[] values;
@Override
public void setParameters(Collection<CompoundVariable> parameters) throws InvalidVariableException {
checkParameterCount(parameters, 0, 2);
values = parameters.toArray(new CompoundVariable[0]);
}
@Override
public String execute(SampleResult previousResult, Sampler currentSampler) {
String prefix = values.length > 0 ? values[0].execute().trim() : "";
if (prefix.isEmpty()) {
prefix = "978";
}
StringBuilder isbn = new StringBuilder(prefix);
ThreadLocalRandom random = ThreadLocalRandom.current();
while (isbn.length() < 12) {
isbn.append(random.nextInt(10));
}
int sum = 0;
for (int i = 0; i < 12; i++) {
sum += (isbn.charAt(i) - '0') * (i % 2 == 0 ? 1 : 3);
}
isbn.append((10 - sum % 10) % 10);
String result = isbn.toString();
addVariableValue(result, values, 1);
return result;
}
@Override
public String getReferenceKey() {
return KEY;
}
@Override
public List<String> getArgumentDesc() {
return DESC;
}
}
Its service file:
# src/main/resources/META-INF/services/org.apache.jmeter.functions.Function
com.example.bookshelf.jmeter.functions.RandomIsbnFunction
The class sits in a package whose name contains .functions.. The 5.6.3 jmeter.properties still ships
classfinder.functions.contain=.functions. and classfinder.functions.notContain=.gui., with a comment saying that
function classes not matching them are not loaded. In the 5.6.3 source, CompoundVariable only logs these two values
and loads every Function it finds, but following the naming costs nothing and keeps the plugin safe with older
releases and customised property files.
Used as the label of the bundled JavaTest client in a short run, each sample gets a fresh number (rows from the
results file):
timeStamp,elapsed,label,...
1791977449081,109,isbn 9799220183907,...
1791977449327,113,isbn 9797458006036,...
Each value is a well-formed ISBN-13: for 9799220183907, the weighted sum of the first twelve digits (weights 1, 3,
1, 3, …) is 103, and (10 - 103 % 10) % 10 = 7, the last digit.
Custom Backend Listener clients
The Backend Listener element, its queue and the three bundled clients are described on
Real-time Monitoring with Backend Listener. A
custom client is a class implementing BackendListenerClient, normally by extending
AbstractBackendListenerClient (since 2.13).
Life cycle and threading
The 5.6.3 BackendListener source explains how a client runs:
-
One client per listener name. When the test starts, the first
BackendListenerinstance with a given name creates the client, a bounded queue (Async Queue size, default 5000) and one daemon worker thread, then callssetupTest(context). FurtherBackendListenerinstances with the same name in the same JVM reuse them, so two listeners that should run separate clients need different names. -
createSampleResultruns on the sampler threads. For every sample, the virtual user’s thread callscreateSampleResult(context, result)and puts the returned object in the queue. Returningnulldrops the sample, and returning a trimmed copy reduces memory; this is the only client code that costs virtual-user time. -
handleSampleResultsruns on the single worker thread. The worker takes everything waiting in the queue and passes it as one list. Because only that thread calls it, fields used only fromhandleSampleResultsneed no locking. The context passed to the worker also contains the listener’s name under the keyTestElement.name. -
When the queue is full the sampler threads block until there is room, so a slow client slows the whole test.
-
teardownTestruns at the end of the test, after the worker has drained the queue.
AbstractBackendListenerClient adds getSamplerMetric(label), getMetricsPerSampler() and getUserMetrics(),
the per-label aggregates the bundled InfluxDB and Graphite clients use, and clears them in its setupTest and
teardownTest, which is why subclasses call super. BackendListenerContext offers the same parameter getters as the
Java Request’s context (getParameter, getIntParameter, getLongParameter) plus getBooleanParameter.
Example: counting SLA breaches per search label
The client counts, for every label that starts with a prefix, how many samples exceeded the search SLA (the
search_sla_ms run property used across this section), and logs the totals when the test ends. A real client would
write them to a database or a message queue instead of the log:
package com.example.bookshelf.jmeter.backend;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Counts, per label, the samples slower than an SLA and logs the totals at the end of the test. */
public class SlaBreachBackendListenerClient extends AbstractBackendListenerClient {
private static final Logger log = LoggerFactory.getLogger(SlaBreachBackendListenerClient.class);
private long slaMs;
private String labelPrefix;
// only the single worker thread calls handleSampleResults, so a plain map is enough
private final Map<String, long[]> counts = new TreeMap<>();
@Override
public Arguments getDefaultParameters() {
Arguments args = new Arguments();
args.addArgument("slaMs", "${__P(search_sla_ms,800)}");
args.addArgument("labelPrefix", "GET /api/books");
return args;
}
@Override
public void setupTest(BackendListenerContext context) throws Exception {
super.setupTest(context);
slaMs = context.getLongParameter("slaMs", 800L);
labelPrefix = context.getParameter("labelPrefix", "");
log.info("SLA breach listener started: sla={} ms, labels starting with '{}'", slaMs, labelPrefix);
}
@Override
public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) {
for (SampleResult result : sampleResults) {
if (!result.getSampleLabel().startsWith(labelPrefix)) {
continue;
}
long[] c = counts.computeIfAbsent(result.getSampleLabel(), k -> new long[2]);
c[0]++;
if (result.getTime() > slaMs) {
c[1]++;
}
}
}
@Override
public SampleResult createSampleResult(BackendListenerContext context, SampleResult result) {
// drop what this client never looks at before it reaches the queue
return result.getSampleLabel().startsWith(labelPrefix) ? result : null;
}
@Override
public void teardownTest(BackendListenerContext context) throws Exception {
counts.forEach((label, c) -> log.info("SLA breaches for '{}': {} of {} samples over {} ms",
label, c[1], c[0], slaMs));
counts.clear();
super.teardownTest(context);
}
}
createSampleResult reads labelPrefix, which is set in setupTest before any sample arrives. It runs on many threads
at once, so it only reads that field and never touches counts.
Service file and plan element (Add → Listener → Backend Listener, then pick the class in Backend Listener implementation):
# src/main/resources/META-INF/services/org.apache.jmeter.visualizers.backend.BackendListenerClient
com.example.bookshelf.jmeter.backend.SlaBreachBackendListenerClient
<BackendListener guiclass="BackendListenerGui" testclass="BackendListener" testname="SLA breaches">
<elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="slaMs" elementType="Argument">
<stringProp name="Argument.name">slaMs</stringProp>
<stringProp name="Argument.value">${__P(search_sla_ms,800)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="labelPrefix" elementType="Argument">
<stringProp name="Argument.name">labelPrefix</stringProp>
<stringProp name="Argument.value">GET /api/books</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="classname">com.example.bookshelf.jmeter.backend.SlaBreachBackendListenerClient</stringProp>
<stringProp name="QUEUE_SIZE">5000</stringProp>
</BackendListener>
<hashTree/>
In a 4-thread, 10-iteration run against a slow local stand-in, jmeter.log shows the life cycle:
INFO o.a.j.v.b.BackendListener: SLA breaches: Starting worker with class: class com.example.bookshelf.jmeter.backend.SlaBreachBackendListenerClient and queue capacity: 5000
INFO c.e.b.j.b.SlaBreachBackendListenerClient: SLA breach listener started: sla=800 ms, labels starting with 'GET /api/books'
INFO c.e.b.j.b.SlaBreachBackendListenerClient: SLA breaches for 'GET /api/books/{id} (TestBean)': 17 of 40 samples over 800 ms
Worked numbers. In the Bookshelf load profile (50 threads, 3 s think time, about 0.2 s per request) each thread
completes one sample every 3.2 s, so the listener receives about 50 / 3.2 ≈ 15.6 samples per second. With the default
queue of 5000 results, the worker could stall for 5000 / 15.6 ≈ 320 s before the first virtual user blocks; a client
that makes one network call per batch is far below that limit. Without the prefix filter in createSampleResult, every
sample of the journey (login, search, view, loan) would enter the queue, not only the searches.
Custom dashboard graphs
The HTML dashboard report builds each chart with a graph consumer, a class extending
org.apache.jmeter.report.processor.graph.AbstractGraphConsumer declared in user.properties. The configuration
properties, the default graphs and the no-code CustomGraphConsumer are covered on
HTML Dashboard Report; this
section writes a new consumer class.
How the report generator uses a consumer
From the 5.6.3 ReportGenerator and HtmlTemplateExporter sources:
-
For each
jmeter.reportgenerator.graph.<graph_id>.classname, the generator loads the class withClass.forName, creates it with its no-argument constructor, callssetName(graph_id)andsetTitle(title). -
Each
property.<name>is converted to a setter name in camel case (set_threshold_msbecomessetThresholdMs) and called with the value converted to the parameter type. A property without a matching setter is skipped with a warning; a value that cannot be converted stops the generation. -
initialize()is called; then every sample of the results file is passed toconsume, which asks the keys selector for the X value (a time bucket for over-time graphs), the series selector for the series names and the value selector for the Y value. Anullvalue is ignored. The aggregator combines the values per bucket. -
The exporter renders a graph whose id starts with
custom_on the Customs Graphs page with a generic template. Any other id needs a matching entry in the report template, so plugin graphs should use acustom_id.
For an over-time chart, AbstractOverTimeGraphConsumer fixes the keys selector to time buckets of granularity ms;
a subclass provides createTimeStampKeysSelector() and createGroupInfos(), and fills initializeExtraResults with
what the template reads (the granularity).
Example: slow samples per second
The consumer plots, per label, how many samples per second took longer than a threshold. It is the shape of a hits per second chart with a value selector that only counts slow samples:
package com.example.bookshelf.jmeter.report;
import java.util.Collections;
import java.util.Map;
import org.apache.jmeter.report.processor.MapResultData;
import org.apache.jmeter.report.processor.TimeRateAggregatorFactory;
import org.apache.jmeter.report.processor.ValueResultData;
import org.apache.jmeter.report.processor.graph.AbstractGraphConsumer;
import org.apache.jmeter.report.processor.graph.AbstractOverTimeGraphConsumer;
import org.apache.jmeter.report.processor.graph.GroupInfo;
import org.apache.jmeter.report.processor.graph.NameSeriesSelector;
import org.apache.jmeter.report.processor.graph.TimeStampKeysSelector;
/** Dashboard graph: samples per second slower than a threshold, one series per label. */
public class SlowSamplesPerSecondGraphConsumer extends AbstractOverTimeGraphConsumer {
private long thresholdMs = 800;
// property.set_threshold_ms=800 in user.properties calls this setter
public void setThresholdMs(long thresholdMs) {
this.thresholdMs = thresholdMs;
}
@Override
protected TimeStampKeysSelector createTimeStampKeysSelector() {
TimeStampKeysSelector keysSelector = new TimeStampKeysSelector();
keysSelector.setSelectBeginTime(true);
return keysSelector;
}
@Override
protected Map<String, GroupInfo> createGroupInfos() {
return Collections.singletonMap(
AbstractGraphConsumer.DEFAULT_GROUP,
new GroupInfo(
new TimeRateAggregatorFactory(),
new NameSeriesSelector(),
// 1 for a slow sample, null (ignored) otherwise
(series, sample) -> !sample.isController() && sample.getElapsedTime() > thresholdMs ? 1.0 : null,
false, false));
}
@Override
protected void initializeExtraResults(MapResultData parentResult) {
parentResult.setResult(RESULT_CTX_GRANULARITY, new ValueResultData(getGranularity()));
}
@Override
public void initialize() {
super.initialize();
((TimeRateAggregatorFactory) getGroupInfos().get(AbstractGraphConsumer.DEFAULT_GROUP)
.getAggregatorFactory()).setGranularity(getGranularity());
}
}
TimeRateAggregatorFactory turns the count in each bucket into a rate per second, as in the bundled
HitsPerSecondGraphConsumer; initialize passes it the granularity because it needs the bucket width to compute the
rate.
The graph is declared in the user.properties used to generate the report:
jmeter.reportgenerator.graph.custom_slowSamples.classname=com.example.bookshelf.jmeter.report.SlowSamplesPerSecondGraphConsumer
jmeter.reportgenerator.graph.custom_slowSamples.title=Bookshelf: samples slower than 800 ms per second
jmeter.reportgenerator.graph.custom_slowSamples.property.set_threshold_ms=800
jmeter.reportgenerator.graph.custom_slowSamples.property.set_granularity=1000
jmeter.reportgenerator.graph.custom_slowSamples.property.set_X_Axis=Over Time
jmeter.reportgenerator.graph.custom_slowSamples.property.set_Y_Axis=Slow samples/s
Source: Generating Report Dashboard: Graph settings (User Manual), AbstractOverTimeGraphConsumer (JMeter Javadoc)
The generic Customs Graphs template takes its axis labels from the set_X_Axis and set_Y_Axis configuration
entries, so the chart is labelled even though the class has no setters for them; the generator logs two harmless
warnings when it creates the consumer:
WARN o.a.j.r.d.ReportGenerator: 'set_Y_Axis' is not a valid property for class 'com.example.bookshelf.jmeter.report.SlowSamplesPerSecondGraphConsumer', skip it
WARN o.a.j.r.d.ReportGenerator: 'set_X_Axis' is not a valid property for class 'com.example.bookshelf.jmeter.report.SlowSamplesPerSecondGraphConsumer', skip it
Add setXAxis(String) and setYAxis(String) methods that store the values if a clean log matters. The class must be
on the class path of the JVM that generates the report: in lib/ext of the installation that runs -e -o or
-g, which is not necessarily a load generator.
Two side effects follow from NameSeriesSelector: a series is created for every label in the results file, including
those with no slow sample (they appear in the legend with no points), and transaction controller samples are excluded
by the value selector. Restrict the labels with jmeter.reportgenerator.exporter.html.series_filter when the plan has
many.
Registration: ServiceLoader and class scanning
What 5.6 changed
Up to 5.5, JMeter found every plugin class by scanning all classes in the jars of lib/ext and search_paths.
JMeter 5.6 started loading plugins with Java’s ServiceLoader instead (PR 5885, "It enables faster startup"), and the
tutorial’s Service lookup section explains the contract:
-
an interface that supports service loading is annotated
@JMeterService; -
a plugin registers an implementation in
META-INF/services/<fully qualified interface name>, one class name per line; the tutorial suggests Google’s@AutoServiceannotation processor to generate the files at build time, as JMeter does for its own functions (@AutoService(Function.class)); -
for backward compatibility JMeter still scans the jars, so a plugin without service files keeps working;
-
a jar that provides all its plugins through services can declare the manifest attribute
JMeter-Skip-Class-Scanning: trueso that JMeter does not scan it.
In 5.6.3, the interfaces annotated @JMeterService that matter for plugin authors are Function,
JavaSamplerClient and BackendListenerClient. The JMeter jars themselves also ship service files for internal
extension points (GUI commands, menu creators, View Results Tree renderers and request views, recorder sampler
creators, access-log parsers), and every 5.6.3 jar in lib/ext carries JMeter-Skip-Class-Scanning: true.
What the skip attribute really skips
The 5.6.3 source makes the attribute narrower than its name suggests, and the difference decides whether a plugin shows up:
| Lookup | How 5.6.3 does it | Effect of JMeter-Skip-Class-Scanning: true |
|---|---|---|
Functions, Java Request clients, Backend Listener clients |
|
The jar is not scanned. Without a service file the class is invisible. |
Samplers, config elements, TestBeans and every other menu entry |
|
None: the jar is still scanned for menu entries. |
Both rows were checked against the example jar. With the attribute set and the Function service file deleted from
the jar, ${__randomIsbn(979,isbn)} was no longer recognised and the sample label stayed as the literal
text isbn ${__randomIsbn(979, with no error in the log; with the service file restored, the function worked
again. The ClassFinder menu scan found BookLookupSampler in the same jar regardless of the attribute.
The practical rules:
-
Ship a service file for every
Function,JavaSamplerClientandBackendListenerClientimplementation. -
Set
JMeter-Skip-Class-Scanning: trueonly when every such implementation has one; it speeds up start-up and does not hide TestBeans or GUI classes. -
Keep TestBeans and GUI classes in
lib/ext(or asearch_pathsentry): they are only found by scanning those paths, not by being somewhere on the class path.
Source: JMeterUtils.java at rel/v5.6.3 (apache/jmeter), MenuFactory.java at rel/v5.6.3 (apache/jmeter)
The general discovery model, lib versus lib/ext and the effect of old plugins without service files are described
on Plugins & Plugins Manager.
Deploying to lib/ext
-
Copy the plugin jar to
lib/extof the JMeter installation, and any third-party library it needs tolib. -
Restart JMeter. Class loaders are built at start-up; a running GUI does not see new jars.
-
Repeat on every machine that runs the plan: in distributed testing each server loads the plan itself, so each one needs the jar (see Installation & Configuration for
search_paths,user.classpathandplugin_dependency_paths). The machine that generates the HTML report needs the graph consumer. -
Check
jmeter.logfor the plugin’s classes, or open the GUI and look for Bookshelf Book Lookup under Add → Sampler and__randomIsbnin Tools → Function Helper Dialog.
cp target/bookshelf-jmeter-ext-1.0.0.jar "$JMETER_HOME/lib/ext/"
jmeter -n -t bookshelf-extensions.jmx -q user.properties \
-Jhost=localhost -Jport=8080 -Jthreads=4 -Jloops=10 \
-l results.jtl -e -o report
Creating summariser <summary>
Created the tree successfully using bookshelf-extensions.jmx
Starting standalone test @ 2026 Oct 14 13:30:48 CEST (1791977448877)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary = 120 in 00:00:10 = 11.7/s Avg: 278 Min: 4 Max: 924 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 13:30:59 CEST (1791977459253)
... end of run
The 120 samples are 4 threads × 10 iterations × 3 samplers (the __randomIsbn probe, the Java Request
BookSearchClient and the BookLookupSampler TestBean). The report folder contains the Customs Graphs page with the
custom_slowSamples chart, and jmeter.log the SLA lines of the Backend Listener client shown above.
Source: Getting Started: Classpath configuration (User Manual), Getting Started: CLI mode (User Manual)
For reproducible generators, do not copy jars by hand: publish the jar to a Maven repository and list it in the
jmeter-maven-plugin’s <jmeterExtensions>, or publish it to a Plugins Manager repository (next section) and install
it with PluginsManagerCMD. Both are described on
Plugins & Plugins Manager; the page on CI/CD with Maven, Docker
and GitHub Actions later in this section builds a generator image that way.
Publishing to the Plugins Manager repository
The JMeter Plugins Manager installs plugins listed in a JSON catalogue. The public catalogue is maintained by the jmeter-plugins.org project, a community project that is not part of Apache JMeter. There are two ways to make a plugin installable with it.
A private catalogue
For a plugin used only inside an organisation, publish the jar on an internal server and add a JSON catalogue to
jpgc.repo.address; the format and the property are shown in
Plugins & Plugins Manager. This needs no
approval from anyone.
The public catalogue
The wiki page Submitting Your Plugin to the Marketplace describes the process: the plugin’s code stays in its own
repository, and the author sends a pull request to undera/jmeter-plugins that adds a descriptor:
-
New plugin: append one entry to
site/dat/repo/various.json(the array of community plugins); do not create a new file. -
New version: add a key to the plugin’s existing
versionsmap. -
Stable download location: each version’s
downloadUrlmust point to Maven Central or to the project’s GitHub releases. -
Version key matches the artifact: the key must match the version in the jar file name (
1.0.0forbookshelf-jmeter-ext-1.0.0.jar). -
Formatting: run
python3 format_repo.pybefore committing; CI checks it withpython3 format_repo.py --check(2-space indentation, final newline). -
Real, reusable value: the plugin should do something not already covered by JMeter or by a plugin in the catalogue.
A descriptor for the example project, if it were published on Maven Central under a public group id:
{
"id": "bookshelf-jmeter-ext",
"name": "Bookshelf JMeter Extensions",
"description": "Book Lookup sampler, __randomIsbn function, SLA breach Backend Listener client and a slow-samples dashboard graph",
"screenshotUrl": "https://github.com/example/bookshelf-jmeter-ext/raw/main/docs/book-lookup.png",
"helpUrl": "https://github.com/example/bookshelf-jmeter-ext",
"vendor": "Bookshelf QA",
"markerClass": "com.example.bookshelf.jmeter.sampler.BookLookupSampler",
"componentClasses": [
"com.example.bookshelf.jmeter.sampler.BookLookupSampler",
"com.example.bookshelf.jmeter.functions.RandomIsbnFunction",
"com.example.bookshelf.jmeter.backend.SlaBreachBackendListenerClient"
],
"versions": {
"1.0.0": {
"changes": "First release",
"downloadUrl": "https://repo1.maven.org/maven2/com/example/bookshelf/bookshelf-jmeter-ext/1.0.0/bookshelf-jmeter-ext-1.0.0.jar",
"depends": ["jmeter-core", "jmeter-java", "jmeter-components"],
"libs": {}
}
}
}
Source: Submitting Your Plugin to the Marketplace (JMeter Plugins wiki), Plugin Repository Descriptor Format (JMeter Plugins wiki)
The fields, from the descriptor format page:
| Field | Meaning |
|---|---|
|
Unique plugin identifier, used by |
|
Shown in the Plugins Manager dialog. |
|
Screenshot and help page links in the dialog. |
|
A class whose presence means the plugin is installed. |
|
The significant classes (elements and GUIs); used to detect the plugin and by
|
|
Optional: a class with a |
|
Optional, default |
|
Where the jar of that version is downloaded from. |
|
Optional release notes. |
|
Map of library name to download URL, installed into |
|
Ids of other catalogue entries this version needs. |
In the public catalogue, depends usually names the pseudo-plugins for JMeter’s own jars: on 2026-10-14,
various.json held 80 entries whose versions list jmeter-core 208 times, jmeter-components 99 times,
jmeter-http 47 times and jmeter-java once. Listing the JMeter modules a plugin compiles against is therefore the
established practice, though the descriptor format page does not document these ids. The componentClasses value also
explains why the example lists the TestBean, the function and the client: those are the class names that can appear
in a .jmx.
The jmeter-plugins.org Developers Guidelines add expectations for plugins whose code lives in the project’s own repository (unit tests, a wiki page per plugin, a link to the wiki and the version in every plugin GUI, example plans built with Dummy Samplers). They are good practice for any public plugin.
Cost on the load generator
Extension code runs inside the virtual users' JVM, so it is part of what the generator measures:
-
Samplers and Java Request clients run on the virtual-user thread. Time spent outside
sampleStart()andsampleEnd()is not in the response time but still delays the next request of that thread, lowering throughput. Build clients and parse parameters once (setupTest, static thread-safe clients), not per sample. -
TestBeans pay for
TestBeanHelper.preparebefore every sample: one evaluation and setter call per property. Keep the number of properties reasonable and mark constants withNOT_EXPRESSION. -
Functions run every time a field containing them is evaluated (no caching since 5.6). A function that allocates, locks or does I/O multiplies that cost by the number of threads and fields.
-
Backend Listener clients run on their own thread, except
createSampleResult, which runs on every sampler thread. A client slower than the sample rate fills the queue and then blocks the virtual users. -
Graph consumers run only during report generation, not during the test.
-
Memory: each thread holds its own clone of every TestBean and its own Java Request client instance; 50 threads mean 50 copies of any per-instance buffer.
From the books to 5.6 to 6.0
| Topic | 2.x / 3.0 era | 5.6.3 and planned 6.0 |
|---|---|---|
Plugin discovery |
Class scanning of |
5.6: |
Element API |
|
5.6: |
Functions |
Value cached per iteration. |
5.6: no caching ( |
Building JMeter itself |
Ant ( |
Gradle ( |
Installing plugins |
Unzip jar sets into |
Plugins Manager 2.0, |
GUI visualizers |
A common kind of plugin. |
Discouraged by the manual in favour of the HTML report and Backend Listener clients. |
Runtime Java and libraries (planned 6.0) |
Java 7 / 8. |
Planned per |
At the time of writing the 6.0 change list does not mention changes to the JavaSamplerClient, Function,
BackendListenerClient or TestBean APIs; re-check changes.xml before the release.
Pitfalls
-
Missing service file with the skip attribute. A function or client in a jar marked
JMeter-Skip-Class-Scanning: truebut without itsMETA-INF/servicesentry is silently ignored; a function call then stays as literal text in requests and labels. -
Bundling JMeter. Packaging
ApacheJMeter_coreor its dependencies into the plugin jar (a fat jar withoutprovidedscope) puts two copies of JMeter classes inlib/ext. Keep every JMeter artifactprovided. -
Wrong Java release. A jar compiled for Java 17 fails on a generator still running Java 8 or 11 with
UnsupportedClassVersionError. -
Shared mutable state. A function instance is shared by all threads; a Backend Listener client is shared by the whole test; a TestBean or Java Request client is per thread, but its
staticfields are not. Make shared state thread-safe. -
Holding the element in the GUI. A GUI class that stores a reference to the element it edited shows or saves data of the wrong element, because JMeter reuses GUI instances.
-
Forgetting
super.configureandmodifyTestElementmust callsuper(name, GUI and test classes);AbstractBackendListenerClientsubclasses must callsuper.setupTestandsuper.teardownTest. -
Renaming classes. The class names are written into every
.jmx(testclass,classname) and into the catalogue’scomponentClasses. Renaming a class or package in a later version breaks existing plans. -
Timing too much. Only the code between
sampleStart()andsampleEnd()is measured; client creation inside that window inflates response times. -
Forgetting the report machine. A custom graph consumer must be in
lib/extof the JMeter that generates the report; otherwise the generator logsUnable to add classand leaves the chart out. -
Not restarting. Copying a new jar into a running JMeter has no effect until the next start.
References
-
Apache JMeter API (Javadoc), in particular AbstractJavaSamplerClient, TestBean, BeanInfoSupport, JMeterGUIComponent, AbstractFunction, AbstractBackendListenerClient, AbstractOverTimeGraphConsumer and JMeterService
-
Apache JMeter source code, tag rel/v5.6.3, including
src/examples -
ApacheJMeter_core 5.6.3 (Maven Central) and ApacheJMeter_java 5.6.3 (Maven Central)
-
Submitting Your Plugin to the Marketplace (JMeter Plugins wiki)
-
various.json, the community plugin catalogue (undera/jmeter-plugins)