Scripting with JSR223 & Groovy
|
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, the latest release on 2026-10-14. It runs on Java 8 and later, and
Java 17 or later is recommended. JMeter 5.6.3 bundles Apache Groovy 3.0.20, which is the engine every example on
this page uses. JMeter 6.0 is not released: the differences noted here are planned per xdocs/changes.xml on the
master branch, which requires Java 17 or later (Java 21 recommended) and moves the bundled Groovy to 5.1.3. The books
on the market target JMeter 2.11 to 3.0 and script with BSF and BeanShell elements; those are covered only to explain
how to migrate away from them. Every field, binding, property and rule below comes from the official component
reference, best-practices chapter, functions and properties references and change log. Where the documentation is
silent and the behaviour is taken from the 5.6.3 source code, the text says so.
Most of a JMeter plan is declarative: a sampler, an extractor, an assertion, each configured through fields. Sooner or
later a plan needs something no field offers: a request body assembled from several variables, a signature computed
over that body, a value shared between thread groups, a custom check on a JSON document, a sample that should not be
reported. JMeter’s answer is the JSR223 family of elements, named after the Java specification for scripting engines
(javax.script). They run a script written in any language that has a JSR223 engine on the classpath, and since
JMeter 3.1 the default and recommended language is Groovy. Used correctly, a Groovy script is compiled once and then
runs at close to the speed of Java code; used carelessly, it is recompiled on every call and becomes the most
expensive element in the plan. This page explains both.
The JSR223 elements
There is one JSR223 element per element family, so a script can run at every point of the sampler’s execution cycle described in Test Plan Elements, Execution Order & Scoping:
| Element | Menu path | What the script does | Return value |
|---|---|---|---|
JSR223 Sampler |
Add → Sampler → JSR223 Sampler |
Performs a sample of its own (a call through a client library, a computation) and produces a |
If not null and the script did not set response data itself, its |
JSR223 PreProcessor |
Add → Pre Processors → JSR223 PreProcessor |
Runs before each sampler in its scope: prepares variables, builds bodies, signs requests, modifies the sampler. |
Ignored. |
JSR223 Timer |
Add → Timer → JSR223 Timer |
Computes a delay before each sampler in its scope. |
Converted to a |
JSR223 PostProcessor |
Add → Post Processors → JSR223 PostProcessor |
Runs after each sampler in its scope: reads the response through |
Ignored. |
JSR223 Assertion |
Add → Assertions → JSR223 Assertion |
Checks the sample result and reports a failure through |
Ignored. |
JSR223 Listener |
Add → Listener → JSR223 Listener |
Receives every sample result in its scope, after assertions. |
Ignored. |
The JSR223 Sampler can also be added from the Add → Sampler menu of a Thread Group or controller, and the PostProcessor
and PreProcessor have the default quick-add shortcuts Ctrl+6 and Ctrl+7 (gui.quick_6 and gui.quick_7 in
jmeter.properties).
The diagram places each element in the cycle of one sampler and shows the bindings that matter most at that point:
sampler = next sampler
prev = previous result"] --> TIM["JSR223 Timer
returns delay in ms"] TIM --> SAM["Sampler (HTTP or JSR223 Sampler)
SampleResult = new result"] SAM --> POST["JSR223 PostProcessor
prev = result just produced"] POST --> ASS["JSR223 Assertion
SampleResult, AssertionResult"] ASS --> LIS["JSR223 Listener
sampleResult, sampleEvent"]
Fields and JMX properties
All six elements share the same five fields. They are TestBean elements, so the .jmx file stores them under the
generic TestBeanGUI and plain property names:
| GUI field | JMX property | Meaning | Required |
|---|---|---|---|
Name |
|
Shown in the tree. Also passed to the script as the |
No |
Comments |
|
Free text, not used at run time. |
No |
Language (Scripting Language) |
|
The JSR223 engine name, for example |
Yes |
Parameters |
|
A string passed to the script as |
No |
Script file (File Name) |
|
A file containing the script. If set, it is used and the Script field is ignored. A relative path is resolved
against the |
No |
Cache compiled script if available |
|
Checkbox, written as |
No |
Script |
|
The script text, edited in a syntax-highlighting editor. Required unless a script file is given. |
Yes, unless a file is set |
A complete JSR223 PreProcessor in a .jmx file:
<JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Build loan body" enabled="true">
<stringProp name="scriptLanguage">groovy</stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="cacheKey">true</stringProp>
<stringProp name="script">vars.put('loanBody', groovy.json.JsonOutput.toJson([bookId: vars.get('bookId'), dueInDays: 14]))</stringProp>
</JSR223PreProcessor>
<hashTree/>
The other elements differ only in the tag and testclass: JSR223Sampler, JSR223PostProcessor, JSR223Timer,
JSR223Assertion and JSR223Listener.
Behaviour of each element
The six elements wrap the same script runner but treat the result and errors differently. The following is taken from the component reference and, where it is silent, from the 5.6.3 source of each element class:
| Element | Result handling | When the script throws |
|---|---|---|
JSR223 Sampler |
Before the script runs, the result is already marked successful with code 200 and message OK, the timer is started
and the script text (or |
The sample fails with response code |
JSR223 PreProcessor / PostProcessor |
No result of their own. |
The error is logged and the thread continues; the sampler still runs (pre) or the next element runs (post). |
JSR223 Timer |
The return value is parsed with |
The error is logged and the delay is 0. A |
JSR223 Assertion |
The script sets |
The assertion is marked as an error (not a failure) with the exception text as message. |
JSR223 Listener |
Receives each sample through |
The error is logged; the sample is still recorded by other listeners. |
In every element, an org.apache.jorphan.util.JMeterStopThreadException, JMeterStopTestException or
JMeterStopTestNowException thrown by the script is not swallowed: since 3.2 it stops the thread, the test or the
test immediately, like the corresponding sampler-error actions.
Source: JSR223 Sampler, JSR223 Timer, JSR223 Assertion and Changes history (bug 60813)
Script bindings
Before each call JMeter puts a set of objects into the script’s bindings. They are ordinary script variables, so a
Groovy script uses them directly without declaring or importing them. In 5.6.3 every JSR223 element receives the
common set below; the component reference lists a subset for some elements, but the shared code in
JSR223TestElement.populateBindings binds all of them for all six.
| Binding | Type | Use |
|---|---|---|
|
|
The current thread’s variables: |
|
|
The JVM-wide properties shared by all threads: |
|
|
The thread’s context: |
|
SLF4J |
Writes to |
|
|
The previous sample result of the thread. In a PostProcessor, Assertion or Listener this is the result just produced; in a PreProcessor, Timer or Sampler it is the result of the sampler before. |
|
|
The current sampler: in a PreProcessor or Timer, the sampler about to run; in a PostProcessor, the one that just ran.
A PreProcessor can cast it to the concrete class (for example |
|
|
The element’s name. |
|
|
The script file name, if any. |
|
|
The evaluated Parameters field. |
|
|
|
|
|
|
Some elements add their own:
| Element | Extra binding | Meaning |
|---|---|---|
JSR223 Sampler |
|
The result being built by this sampler: response data, code, message, success flag, |
JSR223 Assertion |
|
|
JSR223 Listener |
|
The result received (note the lower-case |
The __groovy function, covered below, gets a smaller set: log, ctx, vars, props, threadName,
sampler, prev and OUT.
Source: JSR223 PostProcessor, JSR223 Listener and JMeter Javadoc
Variables and properties, and the difference between them, are explained in
Properties, Variables & Functions. The rule for scripts is
the same: vars is per virtual user and never shared, props is per JVM and shared by every thread.
Performance: compilation and caching
Why Groovy
The JSR223 elements accept any engine, but the best-practices chapter is explicit: for intensive load testing use a
language whose engine implements the Compilable interface, and of the engines usually available only Groovy does.
BeanShell declares Compilable but its implementation throws an exception, so JMeter has an explicit workaround that
treats it as non-compilable. JavaScript is also not compilable through these elements, and on Java 15 and later there
is no JavaScript engine in the JDK at all (Nashorn was removed): JMeter 5.6.3 documents copying a Rhino engine jar into
lib/ext or adding OpenJDK Nashorn as a module to get one back. Velocity and other languages work if their engine jar
is in lib, with their own syntax for bindings (for Velocity, $vars.get("a")).
A non-compilable script is parsed and interpreted on every call, by every thread. A compiled Groovy script is turned into a JVM class once and then executed like Java bytecode.
Source: JSR223 Elements (Best Practices) and Changes (known problems: Nashorn)
How the cache works
The details, from the component reference and the 5.6.3 source:
-
Script files are always compiled when the engine supports it, whatever the checkbox says. The cache key includes the file’s last-modified time, so editing the file between runs, or even during a run, gives a new compilation.
-
Inline scripts are compiled only with the checkbox checked. The key is the MD5 hash of the script text, so two elements with identical text share one compiled class.
-
The hash is computed once per element instance. Every thread has its own copy of each element, and that copy computes the hash on its first call and never again.
-
The cache is JVM-wide and bounded. Its size is the property
jsr223.compiled_scripts_cache_size, default100. Since 5.6 it is a Caffeine cache (before, a commons-collections LRU map); an evicted script is simply recompiled the next time it is needed.
# user.properties: raise only if a plan has more than 100 distinct compiled scripts
jsr223.compiled_scripts_cache_size=200
Why ${var} inside a cached script breaks it
JMeter processes variable and function references in the Script field before the text reaches the engine. With caching on, that has two consequences, which the component reference summarises as "caching would only cache first replacement":
-
Wrong values. A thread hashes the text produced by its first call, for example with
${bookId}replaced by42, compiles it and from then on runs that compiled class. On the next iteration the text would say57, but the thread keeps the hash it computed first, so it keeps running the script with42. Nothing fails; the data is silently wrong. -
A polluted cache. Each thread’s first text is different, so each thread adds its own entry.
With caching off the values are right, but every call is a new text, so every call compiles a new class.
The fix is the same in both cases: keep the script text constant and read changing values at run time with
vars.get("bookId"), or pass them through the Parameters field and read args.
// Wrong: JMeter substitutes the value into the text before Groovy sees it
def id = "${bookId}"
// Right: the text never changes, the value is read on every call
def id = vars.get("bookId")
// Also right: Parameters field = ${bookId} ${__P(dueInDays,14)}
def id = args[0]
def days = args[1] as int
A related trap is Groovy’s own string interpolation. A double-quoted Groovy string such as "Loan ${loanId} created"
is also a JMeter reference as far as JMeter is concerned: if a JMeter variable loanId exists it is substituted into
the text, with the effects above. In inline scripts use concatenation, single-quoted strings, or the brace-less Groovy
form "Loan $id created" with a local variable; in a script file JMeter performs no substitution at all, and
references are passed verbatim to the engine.
Worked example: what caching saves
Take the canonical load profile of this section: 50 threads, a 60 s ramp-up and a 660 s scheduler duration (a 10-minute hold). Suppose one Bookshelf journey takes about 8.5 s including think time (the mean iteration of the Bookshelf mix with Z = 3 s, worked out on the logic controllers page), and contains three JSR223 elements (the body builder, the request signer and an assertion).
-
At full load: 50 threads ÷ 8.5 s ≈ 5.9 journeys/s, so 3 × 5.9 ≈ 18 script executions per second.
-
Over the 10-minute hold: 5.9 × 600 ≈ 3 500 journeys, so about 10 600 script executions.
-
Constant text, caching on: 3 compilations for the whole run, one per distinct script.
-
Caching off, or
${...}in the text with caching off: up to 18 compilations per second and about 10 600 generated classes during the hold. Each compiled Groovy script is a new class, which lives in Metaspace; the defaultHEAPsetting caps Metaspace at 256 MB (-XX:MaxMetaspaceSize=256m), so a long soak can end inOutOfMemoryError: Metaspaceon the load generator rather than in a finding about the server. -
${...}in the text with caching on: 50 threads × 2 elements with references = 100 cache entries, which already fills the default cache of 100 before the third script is counted, and every value after each thread’s first iteration is wrong.
The component reference adds one more JVM detail: if Groovy is used without caching, set the system property
-Dgroovy.use.classvalue=true (for example in JVM_ARGS or bin/setenv.sh) because of a Groovy memory leak
present since 2.4.6.
Checking scripts before the run
Tools → Compile JSR223 Test Elements (since 5.1) compiles every JSR223 element of the open plan and reports syntax
errors in the log, without running the test. Combined with a smoke run of one user, it catches typos before they turn
into 50 threads writing stack traces to jmeter.log.
Source: Changes history (Version 5.1)
Inline scripts or script files
| Aspect | Inline script (Script field) | Script file (Script file field) |
|---|---|---|
Compilation |
Only with Cache compiled script if available checked. |
Always, if the engine is compilable; recompiled when the file changes. |
|
Substituted by JMeter before the engine sees the text (avoid them). |
Passed verbatim to the engine; a JMeter reference is a syntax or logic error. |
Versioning and review |
Stored XML-escaped inside the |
A normal |
Portability |
Travels with the plan. |
Relative paths resolve against the directory JMeter was started from. In distributed testing the file must exist on every load generator, like a CSV file. |
A reasonable rule: one-liners and short glue code inline, anything longer than a screen or shared by several elements in a file next to the plan, with JMeter started from the plan’s directory.
__groovy or a JSR223 element
The __groovy function evaluates a Groovy expression wherever a field accepts a function, for example
${__groovy(vars.get("loanStatus") != "ACTIVE")} in an If or While Controller, or a schedule string in an Open Model
Thread Group. It returns the script’s result as a string and can store it in a variable given as the second argument.
| Aspect | __groovy |
JSR223 element |
|---|---|---|
Where |
Inside any field that accepts functions. |
As an element in the tree, at a fixed point of the execution order. |
Size |
An expression or a few statements; commas must be escaped as |
Any size; no escaping. |
Caching |
Not the JSR223 compiled-script cache or its size property. The function documentation asks for constant text
( |
The cache described above. |
Shared helpers |
The property |
|
Debugging |
Errors are logged as warnings and the function returns an empty string. |
A JSR223 Sampler shows the error in the result (code 500); other elements log it. |
The best-practices chapter suggests developing a function script in a JSR223 Sampler first, then storing it in a Test
Plan variable and calling it as ${__groovy(${RANDOM_NAME})}; commas inside the variable’s value then need no
escaping because the call is parsed before the value is interpolated.
Source: __groovy (Functions), Advanced Groovy Scripting configuration (Properties Reference) and Developing script functions (Best Practices)
The use of __groovy in controller conditions is shown in
Logic Controllers & Transactions, and in schedule strings in
Open Model & Throughput Control.
Load-generator cost
Scripting cost is paid on the load generator, and it competes with the threads that generate traffic. In decreasing order of importance:
-
Recompilation. By far the largest cost, as computed above. Keep the cache on and the text constant.
-
Non-compilable languages. BeanShell interprets every call and keeps one interpreter per element per thread: 50 threads × 3 BeanShell elements = 150 interpreters in memory. JavaScript through Rhino interprets as well.
-
Work per call. A compiled Groovy script still runs whatever it contains. Parsing a 1 MB JSON response with
JsonSlurperin every iteration costs far more than a JSON Extractor that pulls one field. Prefer built-in extractors and assertions (see Correlation & Extractors and Assertions) and use a script when they cannot express the logic. -
Logging.
log.infoat every call from 50 threads writes thousands of lines per minute to a single file. Log atdebugand raise the level only while scripting, or guard withlog.isDebugEnabled(). -
Shared state. A script that synchronises on a shared object, or a JSR223 Listener doing heavy work, serialises threads and lowers the throughput the generator can reach. Keep listeners light; they run in the sampling thread.
-
Random data. Generating large random payloads at run time costs CPU; the best-practices chapter advises preparing test data in a CSV file instead.
The best-practices list for reducing resource requirements simply says "Use the most performing scripting language" and points to the JSR223 section: that is Groovy with caching.
Recipes for the Bookshelf plan
The recipes below extend bookshelf-load.jmx. They assume the variables of the earlier pages: username and
password from users.csv, term from search-terms.csv, token from the login response, bookId from the search
response and loanId from the loan response. Every script is Groovy, cached, and reads variables through vars.
Build a JSON body
POST /api/loans needs a JSON body built from several variables. Writing it by hand in the Body Data field breaks as
soon as a value contains a quote; groovy.json.JsonOutput (part of the groovy-json module bundled with JMeter)
escapes correctly.
-
Select the Create loan HTTP Request and choose Add → Pre Processors → JSR223 PreProcessor. Name it
Build loan body. -
Leave Language on
groovyand Cache compiled script if available checked. -
Enter the script below.
-
In the HTTP Request, open the Body Data tab and enter
${loanBody}; addContent-Type: application/jsonin an HTTP Header Manager.
import groovy.json.JsonOutput
def body = [
bookId : vars.get('bookId'),
borrower : vars.get('username'),
dueInDays : (props.getProperty('dueInDays', '14') as int)
]
vars.put('loanBody', JsonOutput.toJson(body))
log.debug('Loan body for thread {}: {}', ctx.getThreadNum(), vars.get('loanBody'))
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Create loan" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">${loanBody}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.path">/api/loans</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
</HTTPSamplerProxy>
<hashTree>
<JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Build loan body" enabled="true">
<stringProp name="scriptLanguage">groovy</stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="cacheKey">true</stringProp>
<stringProp name="script">import groovy.json.JsonOutput
def body = [
bookId : vars.get('bookId'),
borrower : vars.get('username'),
dueInDays : (props.getProperty('dueInDays', '14') as int)
]
vars.put('loanBody', JsonOutput.toJson(body))
log.debug('Loan body for thread {}: {}', ctx.getThreadNum(), vars.get('loanBody'))</stringProp>
</JSR223PreProcessor>
<hashTree/>
</hashTree>
The PreProcessor sits under the sampler, so it runs only for that sampler. The ${loanBody} reference in the body is
fine: it is a sampler field, not script text. The other HTTP fields are described in
HTTP Requests and JSON APIs in
REST, JSON, GraphQL & SOAP APIs.
Source: JSR223 PreProcessor (Component Reference)
and Groovy documentation (groovy.json.JsonOutput)
Compute an HMAC signature
Suppose a gateway in front of the Bookshelf API requires each write request to carry an HMAC-SHA256 signature of the
method, path, a timestamp and the body. JMeter has no signing element, but the JDK’s javax.crypto.Mac does it in a
few lines. The secret is a run property with a placeholder value, never written in the plan.
Add a second JSR223 PreProcessor named Sign request after Build loan body under the same sampler (pre-processors
run in tree order):
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.StandardCharsets
import org.apache.jorphan.util.JMeterStopTestException
String secret = props.getProperty('hmacSecret')
if (!secret) {
// a misconfigured run should stop, not send 50 threads of unsigned requests
throw new JMeterStopTestException('Property hmacSecret is not set (use -JhmacSecret=...)')
}
String ts = String.valueOf(System.currentTimeMillis())
String body = vars.get('loanBody') ?: ''
String canonical = 'POST\n/api/loans\n' + ts + '\n' + body
Mac mac = Mac.getInstance('HmacSHA256')
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), 'HmacSHA256'))
vars.put('sigTs', ts)
vars.put('signature', mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)).encodeHex().toString())
Then add the headers to the sampler’s HTTP Header Manager: X-Timestamp with value ${sigTs} and X-Signature with
value ${signature}. Run it with a placeholder secret:
jmeter -n -t bookshelf-load.jmx -l results.jtl -JhmacSecret=change-me-local-only -Jthreads=1 -Jduration=60
Mac objects are not thread-safe, which is why the script creates one per call instead of storing it in props;
creating it costs microseconds. Throwing JMeterStopTestException stops the whole test at the first call, which is
the right reaction to a missing property.
Conditionally ignore a result
SampleResult.setIgnore() drops a result from reporting. For a JSR223 Sampler the component reference lists the
effects: the result is not delivered to listeners such as View Results Tree or the summariser, and it is not evaluated
by assertions or post-processors, but it still counts for ${JMeterThread.last_sample_ok} and, since 5.4, for the
Thread Group’s Action to be taken after a Sampler error. Since 4.0 the method can also be called on prev from
JSR223 post-processors and assertions.
A data-only sampler. A JSR223 Sampler that only picks a book id should not appear in the report as a sample with a near-zero response time that drags every average down:
// JSR223 Sampler "Pick a book" (bookIds is shared by the setUp Thread Group, see the next recipe)
def ids = props.get('bookIds')
vars.put('bookId', ids[java.util.concurrent.ThreadLocalRandom.current().nextInt(ids.size())])
SampleResult.setIgnore()
A polling request. The View loan request may be repeated by a While Controller until the loan becomes ACTIVE.
Reporting every intermediate PENDING poll would inflate the request count; a JSR223 PostProcessor under it keeps only
the final one:
// JSR223 PostProcessor under "View loan"
def status = vars.get('loanStatus') // set by a JSON Extractor placed before this post-processor
if (prev.isSuccessful() && status == 'PENDING') {
prev.setIgnore()
}
Use this sparingly: an ignored sample also disappears from the error rate, so ignore only samples that are not part of what the test measures. The usual alternative for "do not report this" is the Flow Control Action sampler, which produces no result at all.
Source: JSR223 Sampler (Component Reference) and Changes history (bug 61806)
Share data through properties
Variables never cross thread boundaries; properties do. The pattern: a setUp Thread Group fetches the catalogue once and publishes the ids as a property; every thread of the main Thread Group reads them.
-
Add Add → Threads (Users) → setUp Thread Group with 1 thread and 1 loop.
-
Under it, an HTTP Request
GET /api/books?query=with a JSON Extractor: variablebookId, path$.items[*].id, Match No.-1, which producesbookId_1tobookId_NandbookId_matchNr. -
Under the HTTP Request, after the extractor, a JSR223 PostProcessor:
// JSR223 PostProcessor in the setUp Thread Group
int n = (vars.get('bookId_matchNr') ?: '0') as int
def ids = (1..<(n + 1)).collect { vars.get('bookId_' + it) }.asImmutable()
if (ids.isEmpty()) {
throw new org.apache.jorphan.util.JMeterStopTestException('No books found; is the Bookshelf API seeded?')
}
props.put('bookIds', ids)
log.info('Published {} book ids', ids.size())
The main Thread Group then uses the Pick a book sampler of the previous recipe, or ${__groovy(props.get('bookIds').size())}
wherever a field needs the count.
Rules for shared data:
-
Publish immutable objects (
asImmutable(), or ajava.util.concurrentcollection when threads must update it).propsitself is a synchronisedHashtable, but the objects inside it are not protected. -
Atomic counters for values that every thread updates:
props.computeIfAbsent('loansCreated', { new java.util.concurrent.atomic.AtomicLong() }).incrementAndGet(). -
Properties are per JVM. In distributed testing each load generator runs its own setUp Thread Group and has its own properties; nothing is shared between generators.
-
Properties outlive the test in the GUI. A value put in the GUI stays until JMeter is restarted; reset it in the setUp Thread Group rather than relying on its absence.
-
Strings for functions.
__Pand__propertyread string values; an object stored withprops.putis read back withprops.getin a script.
Fail a sample with a message
A built-in assertion says "Assertion failed"; a script can say why. The JSR223 Assertion under View loan parses the loan and checks two business rules:
// JSR223 Assertion under "View loan"
import groovy.json.JsonSlurper
if (SampleResult.getResponseCode() != '200') {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage('Expected HTTP 200, got ' + SampleResult.getResponseCode())
return
}
def loan = new JsonSlurper().parseText(SampleResult.getResponseDataAsString())
if (String.valueOf(loan.id) != vars.get('loanId')) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage('Loan id mismatch: expected ' + vars.get('loanId') + ', got ' + loan.id)
} else if (!(loan.status in ['ACTIVE', 'PENDING'])) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage('Unexpected loan status ' + loan.status)
}
The message appears in the failureMessage column of a CSV results file and in the assertion results of View Results
Tree while scripting. The same can be done without an assertion element:
-
in a JSR223 PostProcessor:
prev.setSuccessful(false)andprev.setResponseMessage('...'); -
in a JSR223 Sampler:
SampleResult.setSuccessful(false),SampleResult.setResponseCode('...')andSampleResult.setResponseMessage('...');SampleResult.setStopThread(true)additionally stops the thread.
Prefer the JSR223 Assertion, because assertion failures are counted and reported as such, and because the JSON Assertion or JSON Extractor can usually do half of the job more cheaply. Assertions in general are covered in Assertions.
A custom delay and a light listener
The JSR223 Timer returns a delay in milliseconds and must return an integral value:
// JSR223 Timer: 500 to 1499 ms, returned as a long
return 500L + java.util.concurrent.ThreadLocalRandom.current().nextInt(1000)
Returning 500 + Math.random() * 1000 would produce a Double, which the timer cannot parse as a long, so the delay
would silently be zero and an error would be logged on every call. Timers and their distributions are covered in
Timers & Think Time.
A JSR223 Listener runs for every sample in its scope, in the sampling thread. This one counts failed loan creations per JVM, cheaply:
// JSR223 Listener placed under the "Create loan" sampler
if (!sampleResult.isSuccessful()) {
props.computeIfAbsent('failedLoans', { new java.util.concurrent.atomic.AtomicLong() }).incrementAndGet()
}
A JSR223 Sampler in the tearDown Thread Group can then log props.get('failedLoans').
Source: JSR223 Timer and JSR223 Listener (Component Reference)
Migrating from BeanShell, BSF and JavaScript
Since JMeter 3.1 the documentation advises switching from BeanShell elements to JSR223 elements and from the
__BeanShell function to __groovy. The BeanShell Sampler’s own reference entry says migration to the JSR223 Sampler
with Groovy "is highly recommended for performance, support of new Java features and limited maintenance of the
BeanShell library". BSF elements (Sampler, PreProcessor, PostProcessor, Timer, Assertion, Listener) were deprecated in
3.1 in favour of JSR223; 5.6.3 still ships them, hidden from the menus, and none of them appears in its component
reference. The BeanShell server, which some old
plans start through beanshell.server.port, accepts any command from anyone who can connect and should stay
disabled.
| BeanShell or BSF habit | JSR223 + Groovy equivalent |
|---|---|
|
|
Setting the script variables |
|
|
|
|
A script file shared by several elements, |
Reset bsh.Interpreter before each call to contain memory growth |
Not needed: a JSR223 interpreter state is not kept between calls; bindings are rebuilt each time. |
BeanShell’s missing Java 5 syntax (no generics, no enhanced |
Groovy accepts almost all Java syntax plus its own, so most BeanShell code runs unchanged. |
|
|
A BSF element with JavaScript, or a JSR223 element with |
A JSR223 element with |
A mechanical migration of one element:
-
Add the JSR223 element of the same family next to the old one (for example Add → Post Processors → JSR223 PostProcessor).
-
Copy the script, replace
bsh.argswithargsand the response script variables withSampleResultmethods. -
Replace every
${...}in the text withvars.get(...)or a parameter. -
Keep Cache compiled script if available checked, run Tools → Compile JSR223 Test Elements, and validate with one user.
-
Disable, then delete, the old element.
The books show the old style: Performance Testing with JMeter (PTwJ ch. 3) uses a BSF PostProcessor, and Pro Apache JMeter (Pro ch. 3) uses BeanShell pre- and post-processors. The ideas (prepare data before a request, inspect the response after it) carry over unchanged; the elements do not.
Groovy versions and what changed
The bundled Groovy version, from the change log:
| JMeter | Bundled Groovy | Notes |
|---|---|---|
3.0 |
2.4.6 |
First release that bundles Groovy in |
3.1 to 5.2 |
2.4.x (2.4.7 up to 2.4.16) |
Groovy becomes the default JSR223 language in 3.1. |
5.3 |
3.0.3 |
Listed as an incompatible change: "The update to Groovy 3 might break some old Groovy code"; the change log refers to the Groovy 3 release notes. |
5.4, 5.4.1, 5.5 |
3.0.5, 3.0.7, 3.0.11 |
Patch updates. |
5.6, 5.6.3 |
3.0.17, 3.0.20 |
JMeter 5.6.x stays on Groovy 3; there is no Groovy 4 release of JMeter. |
6.0 (planned) |
5.1.3 |
Per |
The evolution of the JSR223 elements themselves, from the books' JMeter 2.x/3.0 to the planned 6.0:
| Version | Change |
|---|---|
2.4 |
JSR223 Sampler, PreProcessor, PostProcessor, Assertion, Timer and Listener added. |
2.8 / 2.9 |
Script files compiled when the engine is |
3.0 |
Groovy bundled; the cache key is generated automatically, so the field becomes the Cache compiled script if available checkbox. |
3.1 |
Groovy is the default language; the |
3.2 |
Stop exceptions thrown by scripts stop the thread or test. |
4.0 |
New JSR223 elements cache compiled scripts by default; |
5.0 |
|
5.1 |
Tools → Compile JSR223 Test Elements. |
5.3 |
Groovy 3. |
5.6 |
The compiled-script cache moves to Caffeine; |
6.0 (planned) |
Groovy 5.1.3; Rhino 1.9.1 for JSR-223 JavaScript (the 6.0 notes say a JSR-223 JavaScript engine now ships by default, so a separate download is no longer needed); the MongoDB Script sampler is removed and the notes point to a JSR223 Sampler with a MongoDB Java driver as the replacement; Java 17 or later is required. |
Preparing scripts for Groovy 5. The planned jump skips Groovy 4, so both sets of breaking changes apply at once.
Before upgrading, read the Groovy 4.0 and 5.0 release notes; one well-known example from Groovy 4 is the removal of
legacy duplicate packages, so code that still imports groovy.util.XmlSlurper must use groovy.xml.XmlSlurper.
Scripts that stick to vars, props, prev, the JDK and groovy.json are the least exposed. Run Tools → Compile
JSR223 Test Elements and a smoke run on a 6.0 build before trusting a migrated plan.
Pitfalls
-
${...}in a cached script. Wrong values after the first iteration and a polluted cache. Usevars.getor Parameters. -
Groovy GStrings with braces in inline scripts. JMeter treats
"${name}"as its own reference first. -
Caching unchecked. Every call compiles; Metaspace grows. Check the box, and set
-Dgroovy.use.classvalue=trueif a plan must run uncached Groovy. -
Relative script-file paths. Resolved against the directory JMeter was started from, which differs between the GUI, CI and remote servers.
-
A Timer returning a
Doubleornull. The delay is silently 0. -
Exceptions in pre- and post-processors. They are only logged; the sampler and the test carry on. Throw a stop exception when continuing would make the results meaningless.
-
argssplit on single spaces. A parameter containing a space becomes two arguments; pass such values through a variable instead. -
Mutable objects in
props. Shared by all threads without protection; publish immutable or concurrent objects. -
Heavy work in scripts. Parsing large bodies or logging every call costs generator CPU; prefer built-in extractors and assertions.
-
Ignoring samples.
setIgnore()also hides errors; use it only for samples that are not part of the measurement. -
BeanShell and JavaScript in load tests. Interpreted on every call; migrate to Groovy.
References
-
Groovy Reference — the language used by JSR223 scripts, in depth.
-
JSR223 Sampler, JSR223 PreProcessor, JSR223 PostProcessor, JSR223 Timer, JSR223 Assertion and JSR223 Listener (Component Reference)
-
JSON Extractor and HTTP Header Manager (Component Reference)
-
Best Practices (Reducing resource requirements, BeanShell scripting, Developing script functions, JSR223 Elements, Sharing variables)
-
Properties Reference (Advanced Groovy Scripting configuration, Advanced JSR-223 Scripting configuration, BeanShell configuration)
-
Apache JMeter source (5.6.3
JSR223TestElementand the JSR223 element classes,bin/jmeter.properties, andxdocs/changes.xmlon master for planned 6.0 changes) -
JMeter Javadoc (
JMeterVariables,JMeterContext,SampleResult,AssertionResult) -
Groovy documentation, Groovy 4.0 release notes and Groovy 5.0 release notes
-
Bayo Erinle, Performance Testing with JMeter, 2nd ed., Packt, 2015 (JMeter 2.12), ch. 3: scripting with a BSF PostProcessor, used for context only.
-
Sai Matam and Jagdeep Jain, Pro Apache JMeter, Apress, 2017 (JMeter 3.0), ch. 3: BeanShell pre- and post-processors, used for context only.