Chapter 19Lesson 03~180 minutes

Scripting with JSR223 and Groovy for Advanced Test Logic: Configuration, Design Patterns, and Trade-Offs

The best JMeter script is often the one you can delete. Built-in components expose intent in the tree, inherit JMeter scope semantics, require less custom testing, and usually reduce generator risk. The decision is not “code versus no code”; it is which state and behavior should remain declarative and which small irreducible transformation deserves executable code.

Components vs scriptInline vs filevars vs propsShared stateJava plugin boundary

Learning objectives

  • Choose built-in components/functions before JSR223 where semantics are equivalent.
  • Choose inline cached snippets or script files based on size/reuse/testability.
  • Choose vars or props based on ownership/lifetime—not convenience.
  • Prefer immutable/local state over mutable global objects.
  • Choose cached Groovy over BeanShell/JavaScript hot paths.
  • Recognize when a growing Groovy subsystem should become a tested Java library/plugin instead.

1. Mandatory examples remain local/free

Runnable comparisons remain 127.0.0.1:8019, ≤2 threads, ≤600 samples/variant. No managed load cloud, enterprise secret manager, shared performance environment, Kubernetes, paid CI, external API, or third-party plugin is required.

2. Built-in component/function versus JSR223

Built-in component/function JSR223 + Groovy
Prefer for extraction, assertions, counters, timers, CSV, controllers, HTTP configuration, common functions. Use for a transformation/validation/state operation that cannot be expressed clearly/declaratively.
Intent visible in tree and standard docs. Intent hidden in source unless carefully named/documented.
Lower custom test/maintenance surface. Requires code review, unit tests, exception/logging/performance discipline.
Usually easier across CI/remote engines. Script files must exist at correct user.dir/path on every engine.

Do not replace a JSON JMESPath Extractor with JsonSlurper just because Groovy can parse JSON. The built-in component already models that state transition.

3. JMeter function versus JSR223 element

__groovy can be useful for a tiny expression embedded in a field. It has cache-safe bindings such as vars and props. However, once the expression becomes multi-line, reusable, needs unit tests, or has exception branches, prefer a named JSR223 element/script file.

Functions can also obscure execution timing because they are evaluated when the containing field is resolved. A visible PreProcessor/PostProcessor often makes lifecycle clearer.

4. Inline script versus script file

Inline cached script Script file
Good for a very small one-off helper. Better for reusable/multi-line logic and separate code review/unit testing.
Check Cache compiled script if available. JMeter can compile/cache script files when engine supports it.
Avoid ${VAR}/function expansion in source; use bindings. File contents receive JSR223 bindings at runtime; relative path resolves from user.dir.
Lives inside JMX, easy single-artifact portability. Every local/CI/remote engine must receive the file at the expected path.

5. vars versus props

vars props
Per-thread virtual-user state. Process-wide JMeter properties.
Correct for tokens, extracted IDs, canonicalized input, user counters. Correct for immutable run/environment knobs and values intentionally shared within one engine.
Safe from other JMeter threads unless you deliberately share objects elsewhere. All threads can read/write; mutable use needs real synchronization/design.
Not shared across threads/engines. Not distributed shared memory; each remote engine is a separate JVM/property set.

Rule: if you find yourself using props.put('CURRENT_USER_TOKEN', ...), stop and re-evaluate ownership.

6. Immutable local data versus shared state

Local variables inside one script invocation are cheapest to reason about. Thread-local vars are the next boundary. Immutable objects referenced safely can be shared when justified. Mutable shared maps/lists/counters require concurrency-safe types and a clear reason.

Do not use props.put('LIST', new ArrayList()) and let all threads append. Even if it “works” at low load, the object itself is not made thread-safe by living in Properties.

7. Cached Groovy versus slower scripting engines

JMeter best practices recommend a JSR223 language implementing Compilable for intensive load; Groovy does. BeanShell and JavaScript are not recommended for intensive JSR223 paths in current JMeter guidance.

Do not assume a JavaScript engine is present on every modern JDK/JMeter runtime. If your existing plan relies on one, pin/provision the engine explicitly and benchmark it—or migrate small hot-path logic to cached Groovy/built-ins.

8. Groovy helper versus custom Java library/plugin

A 10–50 line helper can stay Groovy. When scripts grow into a shared domain library with many classes, complex parsing, large data structures, frequent allocation, concurrency primitives, or public APIs across dozens of plans, consider moving logic into a separately tested/versioned Java library. A real custom JMeter TestElement/plugin is justified only when new component behavior/UI/lifecycle integration is needed.

That choice adds build/release/classpath/plugin compatibility work. Do not create a plugin merely to avoid learning built-in JMeter components.

9. Cache-size trade-off

The default compiled script cache holds 100 compiled scripts. A normal plan should not need hundreds of dynamically different script bodies. If cache pressure appears because code embeds changing data into script source, fix the source/binding design first rather than increasing the cache.

Only tune jsr223.compiled_scripts_cache_size when evidence shows a legitimate large number of stable scripts and generator memory/cache behavior has been measured.

10. Logging design

Use INFO for setup/version/run summaries and rare lifecycle events, DEBUG for targeted temporary investigation, ERROR with contextual identifiers for genuine failures. Avoid logging full response bodies, tokens, passwords, environment variables, or one INFO line per sample in a hot loop.

Per-sample logs add formatting, locking/appender/file I/O and can enlarge jmeter.log enough to affect the generator.

11. Exception strategy

Pure helper functions should reject invalid inputs explicitly. Do not catch Exception and continue with stale variables. If a script cannot produce valid state, surface the exception/failure and let the plan's defined error policy act.

Catch only when you can add useful context or transform the error into a deliberate domain assertion without losing the original cause.

12. Configuration-layer boundaries

Layer Examples Do not confuse with
JMeter core/test plan JSR223 scope, built-in extractors/assertions, cache checkbox, vars/props Groovy/JVM engine internals or SUT latency.
Java/JVM Groovy classes, heap/GC, JIT, classloading Target service CPU.
OS/network process CPU, file I/O, DNS/sockets Script semantic correctness.
SUT HTTP processing/service timing Generator Groovy overhead.
Plugin/library extra ScriptEngine/JAR/custom Java TestElement JMeter core Groovy support.
CI/container script-file mounts/user.dir, JVM/JAR versions, runner CPU Local test-plan scope.

13. Worked scenario

Requirement: “A response has five JSON fields and one proprietary checksum transformation.”

  • JSON JMESPath Extractors/Assertion handle the five fields/status.
  • One cached Groovy helper computes only the proprietary checksum from thread-local inputs.
  • The checksum helper has separate unit tests.
  • The plan records generator CPU/throughput before/after introducing it.
  • If the checksum library grows or becomes shared across many plans, move it to a tested Java library rather than growing ad-hoc Groovy indefinitely.

14. Decision table

Requirement Preferred approach Reason/evidence
Extract JSON field JSON JMESPath Extractor Visible, standard, no custom code.
Simple per-user count Counter / vars Thread ownership explicit.
Tiny custom normalization Cached JSR223 PreProcessor Irreducible transformation, local vars.
Reusable 40-line helper Script file + unit tests Review/test/cache/CI deployment easier.
Process-wide immutable run ID -J property / props read Shared configuration, no per-sample mutation.
Complex reusable domain library Tested Java library/plugin if justified Stronger structure/performance/versioning than growing scripts.
Hot-path BeanShell/JS Refactor to built-ins or cached Groovy Current JMeter guidance favors Compilable engine.

15. Configured versus achieved load and evidence

Configured load is thread count, loops, timers, target endpoint and intended sample count. Achieved load is completed valid samples/second after Groovy compilation/execution, generator CPU/GC, result/logging overhead, and target service time. Preserve script source/hash, cache/config note, vars/props ownership table, unit-test output, JTL + matching jmeter.log, generator CPU/heap, target event counts/service time, and experimental limitations.

Knowledge check

When should JSON parsing remain built-in?

What is the main portability cost of Script File?

Why isn't props a distributed shared-memory store?

What should you fix before increasing the compiled-script cache?

When should Groovy become Java code?

Next lesson

Diagnose script correctness and generator cost without hiding failures

Lesson 4 engineers cache/substitution, shared-state, logging, exception, path, and SampleResult anti-patterns and repairs each at the smallest responsible layer.

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against current Apache JMeter primary documentation on 2026-09-05. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK; JMeter 5.6.3 requires Java 8+. The JMeter 5.6.3 binary line uses Groovy 3.0.20. Groovy's JSR223 engine implements Compilable. JMeter recommends script files (compiled/cached when supported) or inline script text with Cache compiled script if available checked. The compiled-script cache defaults to jsr223.compiled_scripts_cache_size=100. Do not put changing ${VAR} or JMeter function replacement directly inside cached script text: JMeter expands it before the script reaches the engine, so the first replacement can be captured by the cache. Read runtime state through vars, props, Parameters/args, or other supplied bindings instead. JSR223 Sampler exposes log, Label, FileName, Parameters, args, SampleResult, sampler, ctx, vars, props, and OUT; assertions/processors expose the appropriate current/previous-sample bindings. JMeter explicitly recommends migration from BeanShell to JSR223 + Groovy for performance/support, and Groovy is preferred over non-Compilable scripting engines for intensive load paths.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.