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 SampleResult that listeners record. Also used as a carrier for set-up work.

If not null and the script did not set response data itself, its toString() becomes the response data.

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 long and used as the delay in milliseconds.

JSR223 PostProcessor

Add → Post Processors → JSR223 PostProcessor

Runs after each sampler in its scope: reads the response through prev, sets variables, marks results.

Ignored.

JSR223 Assertion

Add → Assertions → JSR223 Assertion

Checks the sample result and reports a failure through AssertionResult.

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:

flowchart LR PRE["JSR223 PreProcessor
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

testname attribute

Shown in the tree. Also passed to the script as the Label binding, and used in the name of the script’s logger.

No

Comments

TestPlan.comments

Free text, not used at run time.

No

Language (Scripting Language)

scriptLanguage

The JSR223 engine name, for example groovy. The drop-down lists the engines found on the classpath when JMeter starts, as name (language version / engine version); more languages appear when their engine jar is added to lib. Since 3.1 an empty value means Groovy.

Yes

Parameters

parameters

A string passed to the script as Parameters, and split on single spaces into the String[] binding args. Variable and function references here are evaluated on every call, which is the documented way to feed changing values into a cached script.

No

Script file (File Name)

filename

A file containing the script. If set, it is used and the Script field is ignored. A relative path is resolved against the user.dir system property, that is, the directory JMeter was started from, not the directory of the .jmx file. A missing or unreadable file makes the element fail.

No

Cache compiled script if available

cacheKey

Checkbox, written as true or false. When checked and the engine implements javax.script.Compilable, the inline script is compiled once and cached under the MD5 hash of its text. Checked by default for elements created since JMeter 4.0. The component reference still describes this field for the Pre-/PostProcessor, Timer, Assertion and Listener as "Script compilation caching", a unique string; that wording dates from before 3.0. In 5.6.3 any value other than false, including an old unique string, enables caching (source: JSR223TestElement).

No

Script

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 File: <name>) is stored as sampler data. After the script, if the end time was not set (SampleResult.setEndTime is allowed since 5.6), the sample is ended. Unlike the BeanShell Sampler, it does not read ResponseCode or IsSuccess script variables: use SampleResult.setSuccessful(), setResponseCode() and setResponseMessage().

The sample fails with response code 500 and the exception text as response message; the stack trace goes to jmeter.log.

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 Long.parseLong(value.toString()).

The error is logged and the delay is 0. A null return logs a warning and also gives 0. A return value such as 1500.0 (a Double) is not a valid long, so it silently gives no delay.

JSR223 Assertion

The script sets AssertionResult.setFailure(true) and setFailureMessage(...).

The assertion is marked as an error (not a failure) with the exception text as message.

JSR223 Listener

Receives each sample through sampleEvent and sampleResult.

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.

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

vars

JMeterVariables

The current thread’s variables: vars.get("k"), vars.put("k", "v"), vars.remove("k"), vars.putObject("k", obj), vars.getObject("k"), vars.getIteration(). get and put work with strings; putObject stores any object for later scripts of the same thread.

props

JMeterProperties, a java.util.Properties

The JVM-wide properties shared by all threads: props.get("k"), props.put("k", v), props.getProperty("k", "default"). Also holds the values given with -J.

ctx

JMeterContext

The thread’s context: ctx.getThreadNum() (0-based), ctx.getThreadGroup(), ctx.getCurrentSampler(), ctx.getPreviousResult(), ctx.getVariables().

log

SLF4J Logger

Writes to jmeter.log (and the GUI log viewer). The logger name is the element class plus . plus the element name, so log levels can be tuned per element in log4j2.xml.

prev

SampleResult

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.

sampler

Sampler

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 HTTPSamplerProxy) and change it.

Label

String

The element’s name.

FileName

String

The script file name, if any.

Parameters

String

The evaluated Parameters field.

args

String[]

Parameters split on single spaces. A value containing a space is split too.

OUT

PrintStream

System.out: OUT.println("...") writes to the console that started JMeter, which is handy in CLI mode.

Some elements add their own:

Element Extra binding Meaning

JSR223 Sampler

SampleResult

The result being built by this sampler: response data, code, message, success flag, setIgnore(), setStopThread(true), setStopTest(true).

JSR223 Assertion

SampleResult, AssertionResult

SampleResult is the result under test (the same object as prev); AssertionResult receives the verdict.

JSR223 Listener

sampleResult, sampleEvent

The result received (note the lower-case s) and the SampleEvent that carries it, including the thread group name.

The __groovy function, covered below, gets a smaller set: log, ctx, vars, props, threadName, sampler, prev and OUT.

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

flowchart TD A[JSR223 element is called] --> B{Script file set?} B -- yes --> C{Engine is Compilable?} C -- no --> D[Read and evaluate the file on every call] C -- yes --> E["Key: language, absolute path, last-modified time"] E --> H[Compile once, then run the cached class] B -- no --> F{"Engine is Compilable and cacheKey is not false?"} F -- no --> G[Evaluate the script text on every call] F -- yes --> I["Key: MD5 of the script text, computed on the element's first call"] I --> H

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, default 100. 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":

  1. Wrong values. A thread hashes the text produced by its first call, for example with ${bookId} replaced by 42, compiles it and from then on runs that compiled class. On the next iteration the text would say 57, but the thread keeps the hash it computed first, so it keeps running the script with 42. Nothing fails; the data is silently wrong.

  2. 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 default HEAP setting caps Metaspace at 256 MB (-XX:MaxMetaspaceSize=256m), so a long soak can end in OutOfMemoryError: Metaspace on 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.

${...} references

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 .jmx; diffs are hard to read.

A normal .groovy file, reviewable and testable on its own; one file can be reused by several elements.

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 (vars.get, not ${...}) so that Groovy can cache it.

The cache described above.

Shared helpers

The property groovy.utilities names a Groovy file loaded by the function’s engine; the shipped jmeter.properties leaves it commented out with bin/utility.groovy as the example.

jsr223.init.file runs a script once when JMeter starts (since 5.0), with log, props and OUT.

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.

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 JsonSlurper in 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.info at every call from 50 threads writes thousands of lines per minute to a single file. Log at debug and raise the level only while scripting, or guard with log.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.

  1. Select the Create loan HTTP Request and choose Add → Pre Processors → JSR223 PreProcessor. Name it Build loan body.

  2. Leave Language on groovy and Cache compiled script if available checked.

  3. Enter the script below.

  4. In the HTTP Request, open the Body Data tab and enter ${loanBody}; add Content-Type: application/json in 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.

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.

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.

  1. Add Add → Threads (Users) → setUp Thread Group with 1 thread and 1 loop.

  2. Under it, an HTTP Request GET /api/books?query= with a JSON Extractor: variable bookId, path $.items[*].id, Match No. -1, which produces bookId_1 to bookId_N and bookId_matchNr.

  3. 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 a java.util.concurrent collection when threads must update it). props itself is a synchronised Hashtable, 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. __P and __property read string values; an object stored with props.put is read back with props.get in 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) and prev.setResponseMessage('...');

  • in a JSR223 Sampler: SampleResult.setSuccessful(false), SampleResult.setResponseCode('...') and SampleResult.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').

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

bsh.args

args (same split on spaces), or Parameters.

Setting the script variables ResponseCode, ResponseMessage, IsSuccess in a BeanShell Sampler

SampleResult.setResponseCode(), setResponseMessage(), setSuccessful().

bsh.shared namespace for data shared between threads

props with immutable or concurrent objects.

beanshell.*.init initialisation files (beanshell.sampler.init, beanshell.preprocessor.init…​)

A script file shared by several elements, groovy.utilities for __groovy, or jsr223.init.file for start-up code.

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 for)

Groovy accepts almost all Java syntax plus its own, so most BeanShell code runs unchanged.

${var} inside the script text

vars.get('var'), required for caching.

A BSF element with JavaScript, or a JSR223 element with javascript

A JSR223 element with groovy. On Java 15 or later JavaScript needs an extra engine jar anyway.

A mechanical migration of one element:

  1. Add the JSR223 element of the same family next to the old one (for example Add → Post Processors → JSR223 PostProcessor).

  2. Copy the script, replace bsh.args with args and the response script variables with SampleResult methods.

  3. Replace every ${...} in the text with vars.get(...) or a parameter.

  4. Keep Cache compiled script if available checked, run Tools → Compile JSR223 Test Elements, and validate with one user.

  5. 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 lib; remove any older copy from search_paths or user.classpath.

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 changes.xml on master, a two-major-version jump from 3 to 5.

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 Compilable (2.8); inline script compilation and caching added (2.9), which then required a unique cache key string per element.

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 __groovy function is added; BSF elements are deprecated; the documentation advises moving from BeanShell to JSR223.

3.2

Stop exceptions thrown by scripts stop the thread or test.

4.0

New JSR223 elements cache compiled scripts by default; setIgnore() is allowed in post-processors and assertions.

5.0

jsr223.init.file runs a script at start-up.

5.1

Tools → Compile JSR223 Test Elements.

5.3

Groovy 3.

5.6

The compiled-script cache moves to Caffeine; SampleResult.setEndTime is honoured in the JSR223 Sampler.

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. Use vars.get or 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=true if 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 Double or null. 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.

  • args split 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