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]