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]

Reply via email to