Groovy 4.0.30 → 5.0.8 · sandbox fork im-groovy-sandbox 1.19 → 2.0
This is the single highest-risk part of the IM 8.1.0 upgrade for customer integrations, because Groovy is not a build-time detail here — it is the language your mappers, filters, converters and route expressions are written in, and IM compiles and runs those scripts at runtime.
Why this deserves a careful read. Most Groovy 5 changes are not compile errors. A script that deployed fine on IM 8.0 will still deploy on 8.1.0 and then produce a different value. The sections tagged SILENT below are the ones to search your repository for.
How to use this page
-
Work through §1 Silent behaviour changes first — these cannot be caught by deploying, only by reading code or by output diffing.
-
Then §2 Changes that fail loudly — these surface as a failed deployment or a runtime exception, so a deploy of every script is an adequate test.
-
Then §3 Sandbox changes — IM-specific, affects which classes your scripts may touch at all.
-
§4 Search patterns gives concrete greps.
Legend: SILENT = same code, different result, no error · DEPLOY-FAIL = script no longer compiles · RUNTIME = throws when executed.
1. Silent behaviour changes
1.1 File / Path truthiness now means "exists" — SILENT
Groovy 5 changed the Groovy-truth of java.io.File and java.nio.file.Path. In Groovy 4 a non-null File object was truthy. In Groovy 5 the file is truthy only if it exists on disk.
// BEFORE (Groovy 4): true whenever the object is non-null
def f = new File('/data/inbox/orders.csv')
if (f) {
// reached even when the file does not exist yet
}
// AFTER (Groovy 5): false when the file does not exist
if (f) {
// now skipped for a not-yet-created file
}
Impact in IM. Scripts that build an output File and test it before writing, or that null-check a File parameter with if (file), change branch. Nothing throws — the branch is just not taken.
Fix. Be explicit about intent:
if (f != null) // "I have a file object"
if (f.exists()) // "the file is on disk"
Groovy 5.0.7 added a system property to restore the old behaviour, but relying on it only defers the problem — prefer making the test explicit.
1.2 % operator dispatches to remainder() instead of mod() — SILENT
For custom types that define their own mod() method, % no longer routes to it; Groovy 5 looks for remainder() first. The two differ for negative operands (mod is floor-based, remainder is truncation-based).
// A custom type with a mod() implementation
class Cycle {
int value
Cycle mod(Cycle other) { /* floor semantics */ }
}
def r = a % b // Groovy 4 -> a.mod(b)
// Groovy 5 -> a.remainder(b) if present, otherwise falls back
Impact in IM. Only affects scripts that define custom numeric-ish types, or that use % on BigDecimal/BigInteger values where the sign can be negative. Plain int % int is unchanged.
Fix. Call the method explicitly (a.mod(b)) where floor semantics are required.
1.3 Property access on Map-implementing classes now prefers the real field — SILENT
(Apache tickets GROOVY-6144 / GROOVY-5001, fixed in 5.0.0-alpha-1)
For a class that implements Map (or extends HashMap/Properties), dot-property access used to be able to resolve to a map entry and shadow a real declared field. Groovy 5 reorders resolution so a declared field/getter wins; subscript access still always reads the map entry.
class Payload extends HashMap {
String name = 'field-value'
}
def p = new Payload()
p.put('name', 'map-value')
p.name // Groovy 4: could resolve to 'map-value' (and did so inconsistently
// inside closures); Groovy 5: 'field-value'
p['name'] // both versions: 'map-value' <- unambiguous, prefer this
p.getName() // both versions: 'field-value' <- unambiguous, prefer this
Also fixed in the same change: map.class now returns the Class object instead of null, and map['metaClass'] = x no longer throws ClassCastException.
Impact in IM. Mapper and converter scripts that extend HashMap to build a record, or that treat a map-like DTO with both entries and fields, can now read a different value. Nothing throws.
Fix. Use obj['key'] when you mean the map entry and obj.getX() when you mean the property.
1.4 @CompileStatic overload selection realigned with dynamic dispatch — SILENT
(GROOVY-8788, fixed in 5.0.0-alpha-1; the Groovy authors label this a breaking change)
Under @CompileStatic, Groovy 4's static type checker could pick a different overload than the dynamic runtime would for the same call — notably for extension methods with overlapping self/parameter types. Groovy 5 aligns the static checker with runtime dispatch ("prefer closer parameter matching").
Impact in IM. Only scripts annotated @CompileStatic that call overloaded methods where more than one candidate applies. The call still compiles; a different method runs.
Fix. Remove the ambiguity — add an explicit cast at the call site, or avoid overlapping overloads.
1.5 Nest-based access replaces synthetic accessor methods — SILENT (bytecode)
(GROOVY-10687, fixed in 5.0.0-alpha-11)
Groovy 4 generated synthetic access$000-style bridge methods so inner classes and closures could reach private members of their enclosing class. Groovy 5 uses JEP 181 nestmates (NestHost/NestMembers attributes) and no longer emits those bridge methods.
Impact in IM. Invisible for ordinary scripts. It matters if a script (or a helper class deployed as an IM class) uses reflection to enumerate methods and expects the old synthetic accessors, or asserts on a class's exact method inventory.
This is also the area that historically interacted badly with the sandbox — see §3.2.
1.6 Annotation retention changed to SOURCE for AST transforms — SILENT (reflection)
@ToString, @EqualsAndHashCode, @Immutable, @Canonical, @AutoClone, @Sortable and related AST-transform annotations moved from RUNTIME/CLASS retention to SOURCE. The generated code is unchanged; the annotation is simply no longer present on the compiled class.
@ToString
class Order { String id }
// Groovy 4: Order.class.annotations contains ToString
// Groovy 5: it does not — the annotation is gone after compilation
Impact in IM. Only scripts that reflect over their own annotations. toString() itself still works exactly as before.
1.7 Set minus semantics and other collection tweaks — SILENT
Set minus behaviour changed; where the previous element-removal semantics are required, use removeAll. chop() now stops at exhaustion instead of padding with empty lists, and its Iterator variant returns Iterator<List>. findIndexValues() returns an Iterator rather than a List.
def idx = list.findIndexValues { it > 10 }
idx.size() // Groovy 4: List.size()
// Groovy 5: Iterator has no size() -> MissingMethodException
def idxList = list.findIndexValues { it > 10 }.toList() // portable
findIndexValues is the one most likely to throw rather than silently differ, because Iterator lacks the List methods scripts typically call next.
2. Changes that fail loudly
2.1 Duplicate imports are now a compile error — DEPLOY-FAIL
Groovy 4 silently accepted a repeated import (last one won). Groovy 5 rejects it.
import java.time.LocalDate
import java.time.LocalDate // Groovy 5: compilation fails
Impact in IM. Generated or copy-pasted mapper scripts sometimes accumulate duplicate imports. These fail at deploy time, so deploying every script surfaces them all.
2.2 abstract / final on enums is now a compile error — DEPLOY-FAIL
Previously ignored, now rejected. Remove the modifier.
2.3 java.time is auto-imported — DEPLOY-FAIL (name clash)
Groovy 5 adds java.time.* to the default imports. A customer class in the default package named Duration, Period, Instant, Clock etc. now collides.
Fix. Rename the class, or fully-qualify the reference.
2.4 Stricter generics checking under @CompileStatic — DEPLOY-FAIL
(GROOVY-9074, fixed in 5.0.0-alpha-12)
Groovy 5 implements Java-style wildcard capture in the static type checker. Code that was unsafe but previously compiled is now rejected, matching javac:
@CompileStatic
class Holder {
static Collection<?> c = new ArrayList<String>()
static void main(String[] args) {
c.add(new Object()) // Groovy 4: compiled, silently corrupted the list
// Groovy 5: compile error (capture conversion)
}
}
Fix. Use the concrete generic type, or a properly bounded wildcard (<? extends X> / <? super X>).
2.5 Anonymous inner classes are package-private — RUNTIME (reflection only)
(GROOVY-11481, fixed in 5.0.0-alpha-11)
Groovy 5 emits anonymous inner classes with default (package-private) access, matching javac; Groovy 4 marked them public. Cross-package reflective access to such a class can now throw IllegalAccessException. Normal use through the implemented interface is unaffected.
Fix. Use a named, explicitly public class where external reflective access is required.
2.6 Scripts with only a static main no longer extend Script — RUNTIME
Such a script class no longer inherits from groovy.lang.Script, so the binding is not reachable via this. IM mapper/filter expressions are not written this way, but standalone helper scripts might be.
2.7 Removed / changed internals
-
GroovyClassLoader.sourceCachechanged type to theFlexibleEvictableCacheinterface. -
The
$getLookupJPMS workaround hook was removed. -
The spread-dot operator's null handling changed in an edge case.
These affect code that reaches into Groovy internals — rare in customer scripts, relevant for deployed helper classes that do clever things.
3. Sandbox changes (IM-specific)
IM executes customer Groovy inside a sandbox (see groovy-sandbox.md) built on a Pricefx fork of groovy-sandbox. IM 8.1.0 moves that fork from 1.19 → 2.0, which is the Groovy 5 line.
3.1 The Jackson classes on the whitelist changed package — DEPLOY-FAIL
This is a direct consequence of the Jackson 2 → 3 migration (see jackson-2-to-3-migration.md). The sandbox whitelist previously allowed:
com.fasterxml.jackson.databind.JsonNode
com.fasterxml.jackson.core.JsonParser
com.fasterxml.jackson.core.JsonToken
IM 8.1.0 allows the Jackson 3 equivalents instead:
tools.jackson.databind.JsonNode
tools.jackson.core.JsonParser
tools.jackson.core.JsonToken
// BEFORE
import com.fasterxml.jackson.databind.JsonNode
// AFTER
import tools.jackson.databind.JsonNode
A script still importing the com.fasterxml classes is rejected by the sandbox — the class is no longer on the allow-list, so this fails rather than silently working.
3.2 Anonymous inner classes and closures capturing variables
groovy-sandbox.md already documents a restriction here: anonymous inner classes and closures that capture external variables were not permitted under the sandbox on OpenJDK 17/21 with Groovy 4.0.15. Groovy 5's move to nestmates (§1.5) changes exactly the bytecode mechanism this restriction relates to, and the sandbox fork's SandboxTransformer was updated for Groovy 5.
Recommendation. If your scripts use closures that capture external variables — most do — include a representative sample in your pre-production verification. This is the area where the sandbox and the compiler interact most tightly, and it is worth exercising rather than assuming.
3.3 Sandbox fix carried in 2.0
The 2.0 fork also carries PFIMCORE-3104, which fixes super method calls being erased by the sandbox transformer. Scripts that call super.someMethod() behave correctly on 8.1.0 where they may have misbehaved before.
4. Search patterns for your repository
Run these against your customer routes, mappers, filters and deployed Groovy classes.
|
What to find |
Pattern |
Why |
|
|---|---|---|---|
|
Jackson imports |
|
§3.1 — sandbox now allows only |
|
|
File truthiness |
|
§1.1 — meaning changed to "exists" |
|
|
|
|
§1.7 — returns |
|
|
|
|
§1.7 — padding behaviour changed |
|
|
Custom |
`def mod\( |
Object mod\(` |
§1.2 — |
|
|
|
§1.4, §2.4 — overload selection + generics |
|
|
Map-extending classes |
`extends HashMap |
implements Map` |
§1.3 — property resolution reordered |
|
Duplicate imports |
(deploy and read the error) |
§2.1 — fails at deploy |
|
|
Reflection on annotations |
`\.annotations |
getAnnotation` |
§1.6 — AST annotations are SOURCE now |
|
|
|
§3.3 — previously erased by the sandbox |
5. What IM does for you
You do not need to act on these — they are handled inside IM:
-
The sandbox fork, its transformer and the call-site interceptors were rebuilt for Groovy 5.
-
The whitelist itself was migrated (only the Jackson entries changed package).
-
IM's own Groovy code and its ~4 000 tests run on Groovy 5.0.8.
-
Spock moved to
2.4-groovy-5.0— relevant only if you build against IM's test artifacts.
6. Sources
-
Apache JIRA: GROOVY-6144, GROOVY-5001, GROOVY-8788, GROOVY-9074, GROOVY-11481, GROOVY-10687
-
IM:
groovy-sandbox.md,classes-groovy.md