Skip to content

Commit 98e801c

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

File tree

5 files changed

+108
-110
lines changed

5 files changed

+108
-110
lines changed

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

Lines changed: 41 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +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;
3334
import java.util.function.LongSupplier;
35+
import java.util.stream.Stream;
3436

3537
import org.eclipse.persistence.config.QueryHints;
3638
import org.eclipse.persistence.jpa.JpaQuery;
@@ -41,6 +43,8 @@
4143
import org.hibernate.query.SelectionQuery;
4244
import org.jspecify.annotations.Nullable;
4345

46+
import org.springframework.aop.framework.AopProxyUtils;
47+
import org.springframework.aop.support.AopUtils;
4448
import org.springframework.data.util.CloseableIterator;
4549
import org.springframework.transaction.support.TransactionSynchronizationManager;
4650
import org.springframework.util.Assert;
@@ -56,22 +60,15 @@
5660
* @author Jens Schauder
5761
* @author Greg Turnquist
5862
* @author Yuriy Tsarkov
59-
* @author Ariel Morelli Andres (Atlassian US, Inc.)
63+
* @author Ariel Morelli Andres
6064
*/
6165
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
6266

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

7673
@Override
7774
public @Nullable String extractQueryString(Object query) {
@@ -136,9 +133,7 @@ public long getResultCount(Query resultQuery, LongSupplier countSupplier) {
136133
/**
137134
* EclipseLink persistence provider.
138135
*/
139-
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2),
140-
Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
141-
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
136+
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE), List.of(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
142137

143138
@Override
144139
public String extractQueryString(Object query) {
@@ -180,8 +175,7 @@ public String getCommentHintValue(String comment) {
180175
/**
181176
* Unknown special provider. Use standard JPA.
182177
*/
183-
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE),
184-
Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
178+
GENERIC_JPA(List.of(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) {
185179

186180
@Override
187181
public @Nullable String extractQueryString(Object query) {
@@ -231,8 +225,7 @@ public boolean shouldUseAccessorFor(Object entity) {
231225
private static final Collection<PersistenceProvider> ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA);
232226

233227
private static final ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
234-
private final Iterable<String> entityManagerFactoryClassNames;
235-
private final Iterable<String> entityManagerClassNames;
228+
final Iterable<String> entityManagerFactoryClassNames;
236229
private final Iterable<String> metamodelClassNames;
237230

238231
private final boolean present;
@@ -242,37 +235,15 @@ public boolean shouldUseAccessorFor(Object entity) {
242235
*
243236
* @param entityManagerFactoryClassNames the names of the provider specific
244237
* {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty.
245-
* @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not
246-
* be {@literal null} or empty.
247-
* @param metamodelClassNames must not be {@literal null}.
238+
* @param metamodelClassNames the names of the provider specific {@link Metamodel} implementations. Must not be
239+
* {@literal null} or empty.
248240
*/
249-
PersistenceProvider(Iterable<String> entityManagerFactoryClassNames, Iterable<String> entityManagerClassNames,
250-
Iterable<String> metamodelClassNames) {
241+
PersistenceProvider(Collection<String> entityManagerFactoryClassNames, Collection<String> metamodelClassNames) {
251242

252243
this.entityManagerFactoryClassNames = entityManagerFactoryClassNames;
253-
this.entityManagerClassNames = entityManagerClassNames;
254244
this.metamodelClassNames = metamodelClassNames;
255-
256-
boolean present = false;
257-
for (String emfClassName : entityManagerFactoryClassNames) {
258-
259-
if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) {
260-
present = true;
261-
break;
262-
}
263-
}
264-
265-
if (!present) {
266-
for (String entityManagerClassName : entityManagerClassNames) {
267-
268-
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
269-
present = true;
270-
break;
271-
}
272-
}
273-
}
274-
275-
this.present = present;
245+
this.present = Stream.concat(entityManagerFactoryClassNames.stream(), metamodelClassNames.stream())
246+
.anyMatch(it -> ClassUtils.isPresent(it, PersistenceProvider.class.getClassLoader()));
276247
}
277248

278249
/**
@@ -288,32 +259,23 @@ private static PersistenceProvider cacheAndReturn(Class<?> type, PersistenceProv
288259
}
289260

290261
/**
291-
* Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be
262+
* Determines the {@link PersistenceProvider} from the given {@link EntityManager} by introspecting
263+
* {@link EntityManagerFactory} via {@link EntityManager#getEntityManagerFactory()}. If no special one can be
292264
* determined {@link #GENERIC_JPA} will be returned.
265+
* <p>
266+
* This method avoids {@link EntityManager} initialization when using
267+
* {@link org.springframework.orm.jpa.SharedEntityManagerCreator} by accessing
268+
* {@link EntityManager#getEntityManagerFactory()}.
293269
*
294270
* @param em must not be {@literal null}.
295271
* @return will never be {@literal null}.
272+
* @see org.springframework.orm.jpa.SharedEntityManagerCreator
296273
*/
297274
public static PersistenceProvider fromEntityManager(EntityManager em) {
298275

299276
Assert.notNull(em, "EntityManager must not be null");
300277

301-
Class<?> entityManagerType = em.getDelegate().getClass();
302-
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
303-
304-
if (cachedProvider != null) {
305-
return cachedProvider;
306-
}
307-
308-
for (PersistenceProvider provider : ALL) {
309-
for (String entityManagerClassName : provider.entityManagerClassNames) {
310-
if (isEntityManagerOfType(em, entityManagerClassName)) {
311-
return cacheAndReturn(entityManagerType, provider);
312-
}
313-
}
314-
}
315-
316-
return cacheAndReturn(entityManagerType, GENERIC_JPA);
278+
return fromEntityManagerFactory(em.getEntityManagerFactory());
317279
}
318280

319281
/**
@@ -322,12 +284,24 @@ public static PersistenceProvider fromEntityManager(EntityManager em) {
322284
*
323285
* @param emf must not be {@literal null}.
324286
* @return will never be {@literal null}.
287+
* @since 3.5.1
325288
*/
326289
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {
327290

328291
Assert.notNull(emf, "EntityManagerFactory must not be null");
329292

330-
Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
293+
EntityManagerFactory unwrapped = emf;
294+
295+
while (Proxy.isProxyClass(unwrapped.getClass()) || AopUtils.isAopProxy(unwrapped)) {
296+
297+
if (Proxy.isProxyClass(unwrapped.getClass())) {
298+
unwrapped = unwrapped.unwrap(null);
299+
} else if (AopUtils.isAopProxy(unwrapped)) {
300+
unwrapped = (EntityManagerFactory) AopProxyUtils.getSingletonTarget(unwrapped);
301+
}
302+
}
303+
304+
Class<?> entityManagerType = unwrapped.getClass();
331305
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
332306

333307
if (cachedProvider != null) {
@@ -336,8 +310,7 @@ public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory
336310

337311
for (PersistenceProvider provider : ALL) {
338312
for (String emfClassName : provider.entityManagerFactoryClassNames) {
339-
if (isOfType(emf.getPersistenceUnitUtil(), emfClassName,
340-
emf.getPersistenceUnitUtil().getClass().getClassLoader())) {
313+
if (isOfType(unwrapped, emfClassName, unwrapped.getClass().getClassLoader())) {
341314
return cacheAndReturn(entityManagerType, provider);
342315
}
343316
}
@@ -445,16 +418,14 @@ interface Constants {
445418
String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory";
446419
String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager";
447420

448-
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate";
449-
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl";
421+
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory";
450422
String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager";
423+
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
451424

452425
// needed as Spring only exposes that interface via the EM proxy
453-
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl";
454-
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor";
455-
426+
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.SessionFactory";
427+
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.Session";
456428
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
457-
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
458429

459430
}
460431

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: 7 additions & 5 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.
@@ -21,18 +21,19 @@
2121
import org.jspecify.annotations.Nullable;
2222

2323
/**
24-
* {@code CurrentTenantIdentifierResolver} instance for testing
24+
* {@code CurrentTenantIdentifierResolver} instance for testing.
2525
*
26-
* @author Ariel Morelli Andres (Atlassian US, Inc.)
26+
* @author Ariel Morelli Andres
2727
*/
2828
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
29+
2930
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
3031

31-
public static void setTenantIdentifier(String tenantIdentifier) {
32+
static void setTenantIdentifier(String tenantIdentifier) {
3233
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
3334
}
3435

35-
public static void removeTenantIdentifier() {
36+
static void removeTenantIdentifier() {
3637
CURRENT_TENANT_IDENTIFIER.remove();
3738
}
3839

@@ -46,4 +47,5 @@ public String resolveCurrentTenantIdentifier() {
4647
public boolean validateExistingCurrentSessions() {
4748
return true;
4849
}
50+
4951
}

0 commit comments

Comments
 (0)