Add missing proxy hints for Hibernate 8's MutationOrSelectionQuery

Prior to this commit, Hibernate 8 types extending
`MutationOrSelectionQuery` would fail proxying at runtime in native
images because reflection hints were not registered at build time.

While the `MutationOrSelectionQueryImpl` case can be solved with an
additional proxy hint, `NativeMutationOrSelectionQueryImpl` is
impossible to solve that way due to multi interface mismatch.
This was found in gh-36878 and handled with a fallback proxy.

In this case, native image will throw a
`MissingReflectionRegistrationError` - but obviously we cannot depend on
this type in JVM applications. `MissingReflectionRegistrationError`
extends `LinkageError`, which we will use along
IllegalArgumentException` to detect that the proxying operation failed
and that we should use the fallback.

This commit also register a proxy hint for the said fallback.

Fixes gh-37251
This commit is contained in:
Brian Clozel
2026-09-09 10:35:25 +02:00
parent e2fae069dc
commit 280861e7ee
3 changed files with 17 additions and 1 deletions
@@ -18,6 +18,7 @@ package org.springframework.orm.jpa;
import java.util.Collections;
import jakarta.persistence.Query;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.ExecutableMode;
@@ -53,6 +54,10 @@ class EntityManagerRuntimeHints implements RuntimeHintsRegistrar {
// As of Hibernate 8.0
private static final String MUTATION_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.internal.MutationQueryImpl";
// As of Hibernate 8.0
private static final String MUTATION_OR_SELECTION_QUERY_IMPL_CLASS_NAME =
"org.hibernate.query.internal.MutationOrSelectionQueryImpl";
private static final String NATIVE_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.sql.internal.NativeQueryImpl";
private static final String STATELESS_SESSION_CLASS_NAME = "org.hibernate.StatelessSession";
@@ -75,11 +80,15 @@ class EntityManagerRuntimeHints implements RuntimeHintsRegistrar {
builder.onReachableType(SharedEntityManagerCreator.class).withMethod("getMetamodel",
Collections.emptyList(), ExecutableMode.INVOKE);
});
// Fallback proxy for Hibernate 8.0 query implementation types like NativeMutationOrSelectionQueryImpl,
// matching the single-interface fallback in SharedEntityManagerCreator. See gh-36878
hints.proxies().registerJdkProxy(TypeReference.of(Query.class));
}
registerJdkProxyFor(hints, classLoader, QUERY_SQM_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, SQM_QUERY_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, SELECTION_QUERY_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, MUTATION_QUERY_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, MUTATION_OR_SELECTION_QUERY_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, NATIVE_QUERY_IMPL_CLASS_NAME);
registerJdkProxyFor(hints, classLoader, STATELESS_SESSION_CLASS_NAME);
if (ClassUtils.isPresent(PERSISTENCE_UNIT_INFO_DESCRIPTOR_CLASS_NAME, classLoader)) {
@@ -245,8 +245,9 @@ public abstract class SharedEntityManagerCreator {
try {
result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs, ih);
}
catch (IllegalArgumentException ex) {
catch (IllegalArgumentException | LinkageError ex) {
// Hibernate 8.0 NativeMutationOrSelectionQueryImpl multi-interface mismatch?
// IllegalArgumentException on the JVM, LinkageError in native image
// Fall back to single declared interface from method signature.
Class<?>[] singleIfc = new Class<?>[] {method.getReturnType()};
cachedQueryInterfaces.put(query.getClass(), singleIfc);
@@ -17,6 +17,7 @@
package org.springframework.orm.jpa;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
@@ -60,6 +61,11 @@ class EntityManagerRuntimeHintsTests {
assertThat(RuntimeHintsPredicates.proxies().forInterfaces(StatelessSession.class)).accepts(this.hints);
}
@Test
void entityManagerFactoryHasFallbackQueryProxyHints() {
assertThat(RuntimeHintsPredicates.proxies().forInterfaces(Query.class)).accepts(this.hints);
}
@Test
void entityManagerFactoryHasReflectionHints() {
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(EntityManagerFactory.class, "getCriteriaBuilder")).accepts(this.hints);