MartinJesusDev opened a new issue, #16600:
URL: https://github.com/apache/grails-core/issues/16600

   ### Description
   
   `Domain.load(id)` fails to create a lazy proxy for a Groovy domain class 
that implements a
   trait with default field values. The trait's generated `$init$` runs from 
the entity
   constructor while the Hibernate proxy is being instantiated, resolves the 
receiver's metaclass
   through the intercepted `getMetaClass()`, and finds Hibernate's stub value 
(`null`) because the
   interceptor has not been assigned yet.
   
   ### Expected Behavior
   
   `Book.load(id)` returns an uninitialized lazy proxy for a domain class whose 
trait declares
   fields with default values, exactly as it does for a trait without 
initializers (control case).
   
   ### Actual Behaviour
   
       HibernateSystemException: Unable to generate proxy for Book
       Caused by: java.lang.NullPointerException:
           Cannot invoke "groovy.lang.MetaClass.initialize()" because "this.mc" 
is null
           at <Trait>$Trait$Helper.$init$(...)
           at Book.<init>(...)
           at Book$HibernateProxy$...<init>(...)
   
   The exact NPE text varies with the Groovy version; on the current 8.0.x line 
(Groovy 5.1.3) it
   is `Cannot invoke "groovy.lang.MetaClass.invokeMethod(Object, String, 
Object[])" because
   "this.metaClass" is null`, at `<Trait>$Trait$Helper.$init$`.
   
   ### Steps To Reproduce
   
   1. Declare a trait with default values and an entity implementing it:
   
   ```groovy
   trait Auditable {
       String createdBy = 'N/A'
       String lastUpdatedBy = 'N/A'
   }
   
   @Entity
   class Book implements Auditable {
       String title
   }
   ```
   
   2. Persist a `Book` and flush.
   3. Clear the session.
   4. Call `Book.load(id)` in a new transaction/session.
   
   A trait with plain (non-initialized) properties does not reproduce it 
(control case passes).
   
   ### Root Cause
   
   - Hibernate's `groovyGetMetaClassFilter` 
(`ByteBuddyState.ProxyDefinitionHelpers`) only excludes
     `getMetaClass()` from proxy interception when the method is 
`ACC_SYNTHETIC` (HHH-3870 /
     hibernate-orm#2000, 2017). Groovy 3+ no longer marks the generated 
`getMetaClass()` as
     synthetic, so the proxy intercepts it.
   - Hibernate assigns the proxy interceptor after the proxy constructor has run
     (`newInstance()` followed by `$$_hibernate_set_interceptor`), so any 
intercepted method
     invoked from the entity constructor answers `InterceptorDispatcher`'s stub 
value; for
     `getMetaClass()` the stub is `null`.
   - Groovy's trait `$init$` performs a dynamic call that resolves the 
receiver's metaclass through
     `getMetaClass()` and dereferences that `null`.
   - Reproduced in isolation with Hibernate's own `ByteBuddyProxyHelper` 
(without Grails classes),
     and verified with Groovy 3.0/5.1 compiled bytecode (`getMetaClass()` is no 
longer synthetic
     since Groovy 3).
   
   ### Affected Lines
   
   - grails-core `8.0.x` and `8.1.x`: `grails-data-hibernate5` (Hibernate 
5.6.15.Final) and
     `grails-data-hibernate7` (Hibernate 7.4.10.Final), through the 
Grails-owned Groovy proxy stack.
   - Grails `7.0.x` / `7.1.x` / `7.2.x` share the root cause through stock 
Hibernate 5.6 proxies.
     The opt-in `org.yakworks:hibernate-groovy-proxy:1.1` does not avoid it (it 
also assigns the
     interceptor after the proxy constructor). That line has no Grails-owned 
proxy factory to hook
     into, so fixing it would require a change from Hibernate itself or a 
separate feature-level
     change.
   
   ### Proposed Fix (prepared)
   
   A fix is implemented and verified on a branch; a PR against `8.0.x` will 
follow.
   
   - `ByteBuddyGroovyProxyFactory` keeps building Hibernate's proxy class and 
wraps it in a
     Grails-generated subclass that overrides `getMetaClass()`.
   - A new dispatcher answers `InvokerHelper.getMetaClass(persistentClass)` 
while the proxy's own
     answer is `null` (Hibernate's stub during construction) - the same answer
     `GroovyProxyInterceptorLogic` gives for an uninitialized proxy - and 
otherwise returns the
     proxy's answer untouched.
   - Only entities implementing `GroovyObject` get the subclass; non-Groovy 
entities keep the stock
     Hibernate proxy class unchanged.
   - Class loading mirrors each Hibernate line (`ClassLoadingStrategyHelper` on 
5.6,
     `privateLookupIn` on 7.4).
   
   Tests added/updated on both modules: a regression spec for the failing case 
and the control
   case, a unit spec for the dispatcher, and factory specs covering the wrapped 
and unwrapped
   paths. Module suites on the verified machine: `grails-data-hibernate5-core` 
1085 tests /
   0 failures; `grails-data-hibernate7-core` 3650 tests / 0 failures; 
`codeStyle` clean.
   
   Upstream: dropping the `isSynthetic()` requirement from 
`groovyGetMetaClassFilter` (or having
   `InterceptorDispatcher` fall back to the instance implementation while the 
interceptor is not
   assigned) would fix the failure for stock Hibernate users and for the 7.x 
line.
   
   ### Workaround
   
   Do not declare default field values in traits implemented by domain classes; 
assign the
   defaults in the entity constructor or in a persistence event.
   
   ### Environment
   
   - Grails 8.0.0 / 8.0.x (reproduced on the 8.0.x branch)
   - JDK 21 (Liberica 21.0.12)
   - Groovy 5.1.3
   - Hibernate 5.6.15.Final (`grails-data-hibernate5`) and 7.4.10.Final
     (`grails-data-hibernate7`)
   - H2 (in-memory)
   
   ### Investigation Method
   
   The root cause analysis was produced with AI assistance (OpenCode); every 
claim was verified
   locally against the Hibernate 5.6/7.4 sources, Groovy-compiled bytecode, and 
a standalone
   reproduction using Hibernate's own proxy helper.
   


-- 
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