jamesfredley opened a new issue, #16123:
URL: https://github.com/apache/grails-core/issues/16123
### Summary
A Grails 7 plugin that ships a **generic trait containing fields** cannot be
used by a Grails 8 application. The Groovy 4 compiler emitted a **non-generic**
`<Trait>$Trait$FieldHelper` interface for such traits, while the Groovy 5
compiler used by Grails 8 emits a **generic** one and, when compiling an
implementing class, writes a parameterized reference to it.
The result is a class whose `Signature` attribute references
`FieldHelper<T>` while the actual interface on the classpath declares zero type
parameters. The class loads, but any call to `Class.getGenericInterfaces()`
throws:
```
java.lang.reflect.MalformedParameterizedTypeException: Mismatch of count of
formal and actual type arguments in constructor of <Trait>$Trait$FieldHelper: 0
formal argument(s) 1 actual argument(s)
```
`java.beans.Introspector`, Spring, and Jackson all call
`getGenericInterfaces()`, so this fires on ordinary application paths.
Discovered while testing released Grails 7 plugins against `8.0.0-M5`.
### Grails Version
8.0.0-M5
### Java / Groovy Version
Java 21.0.11 (Corretto), Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework
7.0.8
### Real plugin affected
`org.grails.plugins:grails-logical-delete:3.0.0` - the Grails 7 release
announced in #15044. Its public API is the generic trait
`grails.logical.delete.LogicalDelete<D>`, which has fields.
### Steps to Reproduce
1. Generate a stock web app with the published 8.0.0-M5 distribution.
2. Add `implementation "org.grails.plugins:grails-logical-delete:3.0.0"`.
3. Add a domain class implementing the plugin trait:
```groovy
package logical.delete
import grails.logical.delete.LogicalDelete
class LogicalDeletedRecord implements LogicalDelete<LogicalDeletedRecord> {
String name
static constraints = { name nullable: false }
}
```
4. Exercise it through the ordinary public GORM API:
```groovy
@Integration
@Rollback
class LogicalDeleteCompatibilitySpec extends Specification {
void 'delete hides a logical-delete domain instance from public GORM
lookups'() {
given:
LogicalDeletedRecord record = new LogicalDeletedRecord(name:
'release-3.0.0').save(failOnError: true, flush: true)
...
}
}
```
### Actual Behaviour
The plugin resolves and loads - `logicalDelete (3.0.0)` appears in the
plugin list - and the context starts. The test fails on the very first line,
the map constructor:
```
java.lang.reflect.MalformedParameterizedTypeException: Mismatch of count of
formal and actual type arguments in constructor of
grails.logical.delete.LogicalDelete$Trait$FieldHelper: 0 formal argument(s) 1
actual argument(s)
at java.base/java.lang.Class.getGenericInterfaces(Class.java:1298)
at
java.desktop/com.sun.beans.TypeResolver.prepare(TypeResolver.java:312)
at
java.desktop/java.beans.FeatureDescriptor.getParameterTypes(FeatureDescriptor.java:391)
at
java.desktop/java.beans.MethodDescriptor.setMethod(MethodDescriptor.java:118)
at
java.desktop/java.beans.Introspector.getTargetMethodInfo(Introspector.java:1030)
at
java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:446)
at
java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:195)
at
logical.delete.LogicalDeleteCompatibilitySpec.$tt__$spock_feature_0_0(LogicalDeleteCompatibilitySpec.groovy:28)
```
### Root cause evidence
`javap` on the plugin jar (compiled by Groovy 4 for Grails 7) - the trait is
generic but its `FieldHelper` is **not**:
```
public interface grails.logical.delete.LogicalDelete<D extends
java.lang.Object> extends org.grails.datastore.gorm.GormEntity<D>
Signature:
<D:Ljava/lang/Object;>Ljava/lang/Object;Lorg/grails/datastore/gorm/GormEntity<TD;>;
public interface grails.logical.delete.LogicalDelete$Trait$FieldHelper
```
`javap` on the domain class compiled by Grails 8 - it references that
non-generic interface **with a type argument**:
```
public class logical.delete.LogicalDeletedRecord extends java.lang.Object
implements
grails.logical.delete.LogicalDelete<logical.delete.LogicalDeletedRecord>,
grails.logical.delete.LogicalDelete$Trait$FieldHelper<logical.delete.LogicalDeletedRecord>,
org.grails.datastore.mapping.dirty.checking.DirtyCheckable$Trait$FieldHelper,
org.grails.datastore.gorm.GormValidateable$Trait$FieldHelper,
groovy.lang.GroovyObject
```
Note the contrast in the same signature: the Grails 8 (Groovy 5 compiled)
`DirtyCheckable$Trait$FieldHelper` and `GormValidateable$Trait$FieldHelper` are
non-generic traits and are referenced without type arguments, so they are fine.
Confirming that Groovy 5 is self-consistent, this probe compiled inside the
same 8.0.0-M5 app produces a **generic** `FieldHelper`:
```groovy
trait GenericFieldTrait<D> {
String probeField
D probeValue
}
class ProbeUser implements GenericFieldTrait<ProbeUser> {}
```
```
public interface probe.GenericFieldTrait$Trait$FieldHelper<D extends
java.lang.Object>
Signature: <D:Ljava/lang/Object;>Ljava/lang/Object;
public class probe.ProbeUser ... implements
probe.GenericFieldTrait<probe.ProbeUser>,
probe.GenericFieldTrait$Trait$FieldHelper<probe.ProbeUser>,
groovy.lang.GroovyObject
```
So Groovy 5 consistently emits `FieldHelper<D>` and references it as
`FieldHelper<T>`. Against a Groovy 4-compiled plugin whose `FieldHelper` has no
type parameters, the emitted signature is invalid.
### Expected Behaviour
Either a Grails 7 plugin exposing a generic trait with fields remains usable
on Grails 8, or the incompatibility is detected and reported clearly (and
documented), rather than surfacing as a `MalformedParameterizedTypeException`
from JavaBeans introspection.
### Impact
This affects any Grails 7 plugin whose public extension point is a generic
trait carrying fields, which is a common plugin pattern for GORM-facing traits.
The failure mode is poor: resolution, plugin discovery, and startup all
succeed, and the error appears later from an unrelated-looking JDK
introspection frame.
Plugins whose traits are non-generic are unaffected - verified in the same
sweep with `grails-shiro:6.0.0`, whose `GrailsShiroRealm$Trait$FieldHelper` and
`TypedNamedArgs$Trait$FieldHelper` are non-generic and which loads and wires
correctly on 8.0.0-M5.
### Suggested resolution
Options for discussion:
1. Detect the mismatch when Grails compiles a class against a trait whose
`FieldHelper` arity does not match, and emit a clear compile-time error naming
the plugin and trait instead of producing an invalid signature.
2. Raise upstream with Groovy if the Groovy 4 to Groovy 5 `FieldHelper`
generics change is considered a trait ABI break that should be tolerated when
compiling against older trait bytecode.
3. At minimum, document this in the Grails 8 upgrade guide's
plugin-compatibility section: a Grails 7 plugin exposing a generic trait with
fields must be recompiled with Groovy 5 and re-released for Grails 8.
### Notes
Found in a broader sweep of released Grails 7 plugins against 8.0.0-M5.
Related compatibility-shim precedents: #16011 (legacy command plugins) and
#16101 (legacy `Holders` access during `doWithSpring`).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]