apache / apache/grails-core

Grails 7 plugins shipping a Groovy 4-compiled @Immutable class fail on Grails 8 with NoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil

Open Beginner friendly
#16,128 0 comments 0 reactions 0 assignees View on GitHub
relates-to: external-plugin
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

### Summary

Any Grails 7 plugin that ships a Groovy `@Immutable` class compiled with Groovy 4 fails at runtime on Grails 8 with `NoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil`. Groovy 4's `@Immutable` AST transformation emits calls to `org.apache.groovy.runtime.ObjectUtil.cloneObject(Object)` for defensive copying, and that class was removed in Groovy 5.

The plugin resolves, compiles, loads, and boots. The failure only appears when the affected class is actually constructed, so a smoke test that merely starts the application will pass.

Found while auditing released Grails 7 plugins against `8.0.0-M5`, using `io.github.matrei:grails-inertia:3.0.0` (latest release, catalog constraint `7.0.0 > *`).

### Grails Version

8.0.0-M5 (Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8, JDK 21.0.11)

### Steps to Reproduce

In a stock `create-app --profile=web` application on 8.0.0-M5:

```groovy
implementation "io.github.matrei:grails-inertia:3.0.0"
```

Complete the plugin's documented client setup (`npm install && npm run build`, which produces the Vite manifest the plugin requires outside development), add a controller action that calls the plugin's documented `renderInertia(...)`, then issue a real request:

```text
GET /inertia-probe with header X-Inertia: true
```

### Actual Behaviour

The application starts normally - `Tomcat started on port 54517`, and the plugin appears in the load order as `inertia (3.0.0)`. The request returns HTTP 500:

```text
jakarta.servlet.ServletException: Handler dispatch failed:
java.lang.NoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:978)
...
at grails.plugin.inertia.InertiaPage.(InertiaPage.groovy)
at grails.plugin.inertia.Inertia.createJsonModel(Inertia.groovy:148)
at grails.plugin.inertia.InertiaTrait$Trait$Helper.renderInertia(...)
```

### Root Cause

`org.apache.groovy.runtime.ObjectUtil` exists in Groovy 4 and is gone in Groovy 5. Verified directly against the jars on the resolved classpath:

```text
groovy-4.0.32.jar : org/apache/groovy/runtime/ObjectUtil.class present = True
groovy-5.0.8.jar : org/apache/groovy/runtime/ObjectUtil.class present = False
groovy-3.0.25.jar : org/apache/groovy/runtime/ObjectUtil.class present = False
```

The plugin's `InertiaPage` is a Groovy immutable class - its constant pool carries `groovy.transform.KnownImmutable` and a call to `org.codehaus.groovy.transform.ImmutableASTTransformation.checkPropNames`:

```text
#13 = Utf8 Lgroovy/transform/KnownImmutable;
#145 = Utf8 org/codehaus/groovy/transform/ImmutableASTTransformation
41: invokestatic #150 // ImmutableASTTransformation.checkPropNames:(Ljava/lang/Object;Ljava/util/Map;)V
```

and the Groovy 4 `@Immutable` transformation compiled **22** defensive-copy call sites into that single class:

```text
77: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
87: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
102: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
... (22 total)
```

Every one of those `invokestatic` targets is missing at runtime under Groovy 5, so constructing the class throws.

### Expected Behaviour

At minimum this belongs in the Grails 8 upgrade guide's Groovy 5 section: a Groovy 4-compiled `@Immutable` class cannot run on Groovy 5 and must be recompiled. Because the emitted call sites are generated by the AST transformation rather than written by the author, nothing in the plugin's source hints at the dependency, and there is no build-time signal.

### Why this is worth tracking here

The trigger is a plain, idiomatic Groovy annotation, not an exotic API:

- `@Immutable` is ordinary Groovy used widely for value/DTO types.
- The failure survives dependency resolution, compilation, plugin load, and application startup.
- The error names a Groovy internal class and gives no indication that an `@Immutable` class or a stale plugin is responsible.
- It only surfaces when that class is instantiated, so it can hide behind untested code paths.

This is the third distinct Groovy 4 -> 5 binary break found in this sweep, after #16123 (generic trait with fields) and #16126 (AST transformation targeting a private trait method). Together they suggest the upgrade guide would benefit from a consolidated "Groovy 4-compiled plugins can fail at runtime" section, since all three pass every build-time check.

### Distinct from previously reported issues

Not #16122 (Metadata dynamic key access), #16123 (`$Trait$FieldHelper` / `MalformedParameterizedTypeException`), #16124 (partial Jackson 2 classpath), #16125 (pre-Apache `org.grails` coordinates), #16126 (private trait method lost by an AST transformation), or #16127 (Spring 7 `HttpHeaders` no longer implementing `MultiValueMap`). This one is a removed Groovy runtime class referenced by `@Immutable`-generated code.

### Notes

Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5. In the same sweep `cloud.wondrify:i18n-asset-pipeline-grails:5.2.0-M1`, `cloud.wondrify:karman-grails:3.0.6`, and `io.github.gpc:phone-number-constraint:2.0.0` passed verification against their documented public APIs.

Contributor guide

Open the contributing guide

Research direction

Start with the Grails 8 upgrade guide's Groovy 5 section and review the compatibility notes around Groovy 4-compiled plugins. Document that Groovy 4-compiled @Immutable classes can fail when instantiated on Groovy 5, using the reported grails-inertia:3.0.0 reproduction and request path as context. Done means the upgrade guidance clearly warns affected plugin authors to recompile and explains that startup smoke tests may not reveal the failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy
Domain
documentation
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.