From 280861e7ee4a4d8f85b8191dde020484d8ad1b21 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 9 Sep 2026 10:35:25 +0200 Subject: [PATCH] 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 --- .../orm/jpa/EntityManagerRuntimeHints.java | 9 +++++++++ .../orm/jpa/SharedEntityManagerCreator.java | 3 ++- .../orm/jpa/EntityManagerRuntimeHintsTests.java | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java index f836e0911b3..d6c4b2553d3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java @@ -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)) { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index c8046c67e1f..c87f732bc8b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -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); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java index 57df0615ef8..0583779fc5e 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java @@ -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);