Skip to content

Commit 5bd9440

Browse files
committed
Polishing.
Revise PersistenceProvider detection to a EntityManagerFactory-based variant, considering EntityManagerFactory proxying. See: #3425 Original pull request: #3885
1 parent c8221aa commit 5bd9440

File tree

6 files changed

+111
-112
lines changed

6 files changed

+111
-112
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static boolean isMetamodelOfType(Metamodel metamodel, String type) {
5858
return isOfType(metamodel, type, metamodel.getClass().getClassLoader());
5959
}
6060

61-
private static boolean isOfType(Object source, String typeName, @Nullable ClassLoader classLoader) {
61+
static boolean isOfType(Object source, String typeName, @Nullable ClassLoader classLoader) {
6262

6363
Assert.notNull(source, "Source instance must not be null");
6464
Assert.hasText(typeName, "Target type name must not be null or empty");

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 42 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
import jakarta.persistence.metamodel.Metamodel;
2626
import jakarta.persistence.metamodel.SingularAttribute;
2727

28+
import java.lang.reflect.Proxy;
2829
import java.util.Collection;
2930
import java.util.Collections;
3031
import java.util.List;
3132
import java.util.NoSuchElementException;
3233
import java.util.Set;
34+
import java.util.function.LongSupplier;
35+
import java.util.stream.Stream;
3336

3437
import org.eclipse.persistence.config.QueryHints;
3538
import org.eclipse.persistence.jpa.JpaQuery;
@@ -38,6 +41,8 @@
3841
import org.hibernate.ScrollableResults;
3942
import org.hibernate.proxy.HibernateProxy;
4043

44+
import org.springframework.aop.framework.AopProxyUtils;
45+
import org.springframework.aop.support.AopUtils;
4146
import org.springframework.data.util.CloseableIterator;
4247
import org.springframework.lang.Nullable;
4348
import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -54,22 +59,15 @@
5459
* @author Jens Schauder
5560
* @author Greg Turnquist
5661
* @author Yuriy Tsarkov
57-
* @author Ariel Morelli Andres (Atlassian US, Inc.)
62+
* @author Ariel Morelli Andres
5863
*/
5964
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
6065

6166
/**
6267
* Hibernate persistence provider.
63-
* <p>
64-
* Since Hibernate 4.3 the location of the HibernateEntityManager moved to the org.hibernate.jpa package. In order to
65-
* support both locations we interpret both classnames as a Hibernate {@code PersistenceProvider}.
66-
*
67-
* @see <a href="https://github.com/spring-projects/spring-data-jpa/issues/846">DATAJPA-444</a>
6868
*/
69-
HIBERNATE(//
70-
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), //
71-
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), //
72-
Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) {
69+
HIBERNATE(List.of(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), //
70+
List.of(HIBERNATE_JPA_METAMODEL_TYPE)) {
7371

7472
@Override
7573
public String extractQueryString(Query query) {
@@ -117,9 +115,7 @@ public String getCommentHintKey() {
117115
/**
118116
* EclipseLink persistence provider.
119117
*/
120-
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2),
121-
Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
122-
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
118+
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE), List.of(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
123119

124120
@Override
125121
public String extractQueryString(Query query) {
@@ -157,8 +153,7 @@ public String getCommentHintValue(String comment) {
157153
/**
158154
* Unknown special provider. Use standard JPA.
159155
*/
160-
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE),
161-
Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
156+
GENERIC_JPA(List.of(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) {
162157

163158
@Nullable
164159
@Override
@@ -205,8 +200,7 @@ public String getCommentHintKey() {
205200
private static final Collection<PersistenceProvider> ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA);
206201

207202
private static final ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
208-
private final Iterable<String> entityManagerFactoryClassNames;
209-
private final Iterable<String> entityManagerClassNames;
203+
final Iterable<String> entityManagerFactoryClassNames;
210204
private final Iterable<String> metamodelClassNames;
211205

212206
private final boolean present;
@@ -216,37 +210,15 @@ public String getCommentHintKey() {
216210
*
217211
* @param entityManagerFactoryClassNames the names of the provider specific
218212
* {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty.
219-
* @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not
220-
* be {@literal null} or empty.
221-
* @param metamodelClassNames must not be {@literal null}.
213+
* @param metamodelClassNames the names of the provider specific {@link Metamodel} implementations. Must not be
214+
* {@literal null} or empty.
222215
*/
223-
PersistenceProvider(Iterable<String> entityManagerFactoryClassNames, Iterable<String> entityManagerClassNames,
224-
Iterable<String> metamodelClassNames) {
216+
PersistenceProvider(Collection<String> entityManagerFactoryClassNames, Collection<String> metamodelClassNames) {
225217

226218
this.entityManagerFactoryClassNames = entityManagerFactoryClassNames;
227-
this.entityManagerClassNames = entityManagerClassNames;
228219
this.metamodelClassNames = metamodelClassNames;
229-
230-
boolean present = false;
231-
for (String emfClassName : entityManagerFactoryClassNames) {
232-
233-
if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) {
234-
present = true;
235-
break;
236-
}
237-
}
238-
239-
if (!present) {
240-
for (String entityManagerClassName : entityManagerClassNames) {
241-
242-
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
243-
present = true;
244-
break;
245-
}
246-
}
247-
}
248-
249-
this.present = present;
220+
this.present = Stream.concat(entityManagerFactoryClassNames.stream(), metamodelClassNames.stream())
221+
.anyMatch(it -> ClassUtils.isPresent(it, PersistenceProvider.class.getClassLoader()));
250222
}
251223

252224
/**
@@ -262,32 +234,23 @@ private static PersistenceProvider cacheAndReturn(Class<?> type, PersistenceProv
262234
}
263235

264236
/**
265-
* Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be
237+
* Determines the {@link PersistenceProvider} from the given {@link EntityManager} by introspecting
238+
* {@link EntityManagerFactory} via {@link EntityManager#getEntityManagerFactory()}. If no special one can be
266239
* determined {@link #GENERIC_JPA} will be returned.
240+
* <p>
241+
* This method avoids {@link EntityManager} initialization when using
242+
* {@link org.springframework.orm.jpa.SharedEntityManagerCreator} by accessing
243+
* {@link EntityManager#getEntityManagerFactory()}.
267244
*
268245
* @param em must not be {@literal null}.
269246
* @return will never be {@literal null}.
247+
* @see org.springframework.orm.jpa.SharedEntityManagerCreator
270248
*/
271249
public static PersistenceProvider fromEntityManager(EntityManager em) {
272250

273251
Assert.notNull(em, "EntityManager must not be null");
274252

275-
Class<?> entityManagerType = em.getDelegate().getClass();
276-
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
277-
278-
if (cachedProvider != null) {
279-
return cachedProvider;
280-
}
281-
282-
for (PersistenceProvider provider : ALL) {
283-
for (String entityManagerClassName : provider.entityManagerClassNames) {
284-
if (isEntityManagerOfType(em, entityManagerClassName)) {
285-
return cacheAndReturn(entityManagerType, provider);
286-
}
287-
}
288-
}
289-
290-
return cacheAndReturn(entityManagerType, GENERIC_JPA);
253+
return fromEntityManagerFactory(em.getEntityManagerFactory());
291254
}
292255

293256
/**
@@ -296,12 +259,24 @@ public static PersistenceProvider fromEntityManager(EntityManager em) {
296259
*
297260
* @param emf must not be {@literal null}.
298261
* @return will never be {@literal null}.
262+
* @since 3.5.1
299263
*/
300264
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {
301265

302266
Assert.notNull(emf, "EntityManagerFactory must not be null");
303267

304-
Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
268+
EntityManagerFactory unwrapped = emf;
269+
270+
while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) {
271+
272+
if (Proxy.isProxyClass(unwrapped.getClass())) {
273+
unwrapped = unwrapped.unwrap(null);
274+
} else if (AopUtils.isAopProxy(unwrapped)) {
275+
unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped);
276+
}
277+
}
278+
279+
Class<?> entityManagerType = unwrapped.getClass();
305280
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
306281

307282
if (cachedProvider != null) {
@@ -310,8 +285,7 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory
310285

311286
for (PersistenceProvider provider : ALL) {
312287
for (String emfClassName : provider.entityManagerFactoryClassNames) {
313-
if (isOfType(emf.getPersistenceUnitUtil(), emfClassName,
314-
emf.getPersistenceUnitUtil().getClass().getClassLoader())) {
288+
if (isOfType(unwrapped, emfClassName, unwrapped.getClass().getClassLoader())) {
315289
return cacheAndReturn(entityManagerType, provider);
316290
}
317291
}
@@ -408,16 +382,14 @@ interface Constants {
408382
String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory";
409383
String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager";
410384

411-
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate";
412-
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl";
385+
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory";
413386
String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager";
387+
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
414388

415389
// needed as Spring only exposes that interface via the EM proxy
416-
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl";
417-
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor";
418-
390+
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.SessionFactory";
391+
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.Session";
419392
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
420-
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
421393

422394
}
423395

spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@
1616
package org.springframework.data.jpa.provider;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
1920
import static org.springframework.data.jpa.provider.PersistenceProvider.*;
2021
import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*;
2122

2223
import jakarta.persistence.EntityManager;
24+
import jakarta.persistence.EntityManagerFactory;
2325

2426
import java.util.Arrays;
2527
import java.util.Map;
2628

27-
import org.assertj.core.api.Assumptions;
28-
import org.hibernate.Version;
2929
import org.junit.jupiter.api.BeforeEach;
3030
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.params.ParameterizedTest;
32+
import org.junit.jupiter.params.provider.EnumSource;
3133
import org.mockito.Mockito;
3234

3335
import org.springframework.asm.ClassWriter;
@@ -42,6 +44,7 @@
4244
* @author Thomas Darimont
4345
* @author Oliver Gierke
4446
* @author Jens Schauder
47+
* @author Mark Paluch
4548
*/
4649
class PersistenceProviderUnitTests {
4750

@@ -56,12 +59,32 @@ void setup() {
5659
this.shadowingClassLoader = new ShadowingClassLoader(getClass().getClassLoader());
5760
}
5861

62+
@ParameterizedTest // GH-3425
63+
@EnumSource(PersistenceProvider.class)
64+
void entityManagerFactoryClassNamesAreInterfaces(PersistenceProvider provider) throws ClassNotFoundException {
65+
66+
for (String className : provider.entityManagerFactoryClassNames) {
67+
assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isTrue();
68+
}
69+
}
70+
71+
@ParameterizedTest // GH-3425
72+
@EnumSource(PersistenceProvider.class)
73+
void metaModelNamesExist(PersistenceProvider provider) throws ClassNotFoundException {
74+
75+
for (String className : provider.entityManagerFactoryClassNames) {
76+
assertThat(ClassUtils.forName(className, PersistenceProvider.class.getClassLoader()).isInterface()).isNotNull();
77+
}
78+
}
79+
5980
@Test
6081
void detectsEclipseLinkPersistenceProvider() throws Exception {
6182

6283
shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa");
6384

6485
EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE);
86+
when(em.getEntityManagerFactory())
87+
.thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE));
6588

6689
assertThat(fromEntityManager(em)).isEqualTo(ECLIPSELINK);
6790
}
@@ -70,31 +93,19 @@ void detectsEclipseLinkPersistenceProvider() throws Exception {
7093
void fallbackToGenericJpaForUnknownPersistenceProvider() throws Exception {
7194

7295
EntityManager em = mockProviderSpecificEntityManagerInterface("foo.bar.unknown.jpa.JpaEntityManager");
96+
when(em.getEntityManagerFactory()).thenReturn(mock(EntityManagerFactory.class));
7397

7498
assertThat(fromEntityManager(em)).isEqualTo(GENERIC_JPA);
7599
}
76100

77-
@Test // DATAJPA-1019
78-
void detectsHibernatePersistenceProviderForHibernateVersion52() throws Exception {
79-
80-
Assumptions.assumeThat(Version.getVersionString()).startsWith("5.2");
81-
82-
shadowingClassLoader.excludePackage("org.hibernate");
83-
84-
EntityManager em = mockProviderSpecificEntityManagerInterface(HIBERNATE_ENTITY_MANAGER_INTERFACE);
85-
86-
assertThat(fromEntityManager(em)).isEqualTo(HIBERNATE);
87-
}
88-
89101
@Test // DATAJPA-1379
90102
void detectsProviderFromProxiedEntityManager() throws Exception {
91103

92104
shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa");
93105

94-
EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE);
95-
96106
EntityManager emProxy = Mockito.mock(EntityManager.class);
97-
Mockito.when(emProxy.getDelegate()).thenReturn(em);
107+
when(emProxy.getEntityManagerFactory())
108+
.thenReturn(mockProviderSpecificEntityManagerFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE));
98109

99110
assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK);
100111
}
@@ -105,13 +116,23 @@ private EntityManager mockProviderSpecificEntityManagerInterface(String interfac
105116
EntityManager.class);
106117

107118
EntityManager em = (EntityManager) Mockito.mock(providerSpecificEntityManagerInterface);
108-
Mockito.when(em.getDelegate()).thenReturn(em); // delegate is used to determine the classloader of the provider
109-
// specific interface, therefore we return the proxied
110-
// EntityManager.
119+
120+
// delegate is used to determine the classloader of the provider
121+
// specific interface, therefore we return the proxied EntityManager
122+
when(em.getDelegate()).thenReturn(em);
111123

112124
return em;
113125
}
114126

127+
private EntityManagerFactory mockProviderSpecificEntityManagerFactoryInterface(String interfaceName)
128+
throws ClassNotFoundException {
129+
130+
Class<?> providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader,
131+
EntityManager.class);
132+
133+
return (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerInterface);
134+
}
135+
115136
static class InterfaceGenerator implements Opcodes {
116137

117138
static Class<?> generate(final String interfaceName, ClassLoader parentClassLoader, final Class<?>... interfaces)

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/HibernateCurrentTenantIdentifierResolver.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2011-2025 the original author or authors.
2+
* Copyright 2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,21 +18,21 @@
1818
import java.util.Optional;
1919

2020
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
21-
import org.jspecify.annotations.Nullable;
2221

2322
/**
24-
* {@code CurrentTenantIdentifierResolver} instance for testing
23+
* {@code CurrentTenantIdentifierResolver} instance for testing.
2524
*
26-
* @author Ariel Morelli Andres (Atlassian US, Inc.)
25+
* @author Ariel Morelli Andres
2726
*/
2827
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
29-
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
3028

31-
public static void setTenantIdentifier(String tenantIdentifier) {
29+
private static final ThreadLocal<String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
30+
31+
static void setTenantIdentifier(String tenantIdentifier) {
3232
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
3333
}
3434

35-
public static void removeTenantIdentifier() {
35+
static void removeTenantIdentifier() {
3636
CURRENT_TENANT_IDENTIFIER.remove();
3737
}
3838

@@ -46,4 +46,5 @@ public String resolveCurrentTenantIdentifier() {
4646
public boolean validateExistingCurrentSessions() {
4747
return true;
4848
}
49+
4950
}

0 commit comments

Comments
 (0)