Prevent access to EntityManager when looking up PersistenceProvider.
Signed-off-by: Ariel Morelli Andres <amorelliandres@atlassian.com> Closes: #3425 Original pull request: #3885
This commit is contained in:
@@ -19,6 +19,7 @@ import static org.springframework.data.jpa.provider.JpaClassUtils.*;
|
||||
import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.metamodel.IdentifiableType;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
@@ -36,6 +37,7 @@ import org.eclipse.persistence.queries.ScrollableCursor;
|
||||
import org.hibernate.ScrollMode;
|
||||
import org.hibernate.ScrollableResults;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
|
||||
import org.springframework.data.util.CloseableIterator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
@@ -52,6 +54,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
* @author Jens Schauder
|
||||
* @author Greg Turnquist
|
||||
* @author Yuriy Tsarkov
|
||||
* @author Ariel Morelli Andres (Atlassian US, Inc.)
|
||||
*/
|
||||
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
|
||||
|
||||
@@ -64,6 +67,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
* @see <a href="https://github.com/spring-projects/spring-data-jpa/issues/846">DATAJPA-444</a>
|
||||
*/
|
||||
HIBERNATE(//
|
||||
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), //
|
||||
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), //
|
||||
Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) {
|
||||
|
||||
@@ -71,7 +75,6 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
public String extractQueryString(Query query) {
|
||||
return HibernateUtils.getHibernateQuery(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return custom placeholder ({@code *}) as Hibernate does create invalid queries for count queries for objects with
|
||||
* compound keys.
|
||||
@@ -114,7 +117,8 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
/**
|
||||
* EclipseLink persistence provider.
|
||||
*/
|
||||
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
|
||||
ECLIPSELINK(List.of(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1, ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2),
|
||||
Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
|
||||
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
|
||||
|
||||
@Override
|
||||
@@ -147,12 +151,14 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
public String getCommentHintValue(String comment) {
|
||||
return "/* " + comment + " */";
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Unknown special provider. Use standard JPA.
|
||||
*/
|
||||
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
|
||||
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE),
|
||||
Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -199,6 +205,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
private static final Collection<PersistenceProvider> ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA);
|
||||
|
||||
private static final ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
|
||||
private final Iterable<String> entityManagerFactoryClassNames;
|
||||
private final Iterable<String> entityManagerClassNames;
|
||||
private final Iterable<String> metamodelClassNames;
|
||||
|
||||
@@ -207,24 +214,38 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
/**
|
||||
* Creates a new {@link PersistenceProvider}.
|
||||
*
|
||||
* @param entityManagerFactoryClassNames the names of the provider specific
|
||||
* {@link jakarta.persistence.EntityManagerFactory} implementations. Must not be {@literal null} or empty.
|
||||
* @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not
|
||||
* be {@literal null} or empty.
|
||||
* @param metamodelClassNames must not be {@literal null}.
|
||||
*/
|
||||
PersistenceProvider(Iterable<String> entityManagerClassNames, Iterable<String> metamodelClassNames) {
|
||||
PersistenceProvider(Iterable<String> entityManagerFactoryClassNames, Iterable<String> entityManagerClassNames,
|
||||
Iterable<String> metamodelClassNames) {
|
||||
|
||||
this.entityManagerFactoryClassNames = entityManagerFactoryClassNames;
|
||||
this.entityManagerClassNames = entityManagerClassNames;
|
||||
this.metamodelClassNames = metamodelClassNames;
|
||||
|
||||
boolean present = false;
|
||||
for (String entityManagerClassName : entityManagerClassNames) {
|
||||
for (String emfClassName : entityManagerFactoryClassNames) {
|
||||
|
||||
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
|
||||
if (ClassUtils.isPresent(emfClassName, PersistenceProvider.class.getClassLoader())) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!present) {
|
||||
for (String entityManagerClassName : entityManagerClassNames) {
|
||||
|
||||
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.present = present;
|
||||
}
|
||||
|
||||
@@ -269,6 +290,36 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
return cacheAndReturn(entityManagerType, GENERIC_JPA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the {@link PersistenceProvider} from the given {@link EntityManagerFactory}. If no special one can be
|
||||
* determined {@link #GENERIC_JPA} will be returned.
|
||||
*
|
||||
* @param emf must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {
|
||||
|
||||
Assert.notNull(emf, "EntityManagerFactory must not be null");
|
||||
|
||||
Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
|
||||
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
|
||||
|
||||
if (cachedProvider != null) {
|
||||
return cachedProvider;
|
||||
}
|
||||
|
||||
for (PersistenceProvider provider : ALL) {
|
||||
for (String emfClassName : provider.entityManagerFactoryClassNames) {
|
||||
if (isOfType(emf.getPersistenceUnitUtil(), emfClassName,
|
||||
emf.getPersistenceUnitUtil().getClass().getClassLoader())) {
|
||||
return cacheAndReturn(entityManagerType, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cacheAndReturn(entityManagerType, GENERIC_JPA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the {@link PersistenceProvider} from the given {@link Metamodel}. If no special one can be determined
|
||||
* {@link #GENERIC_JPA} will be returned.
|
||||
@@ -354,13 +405,20 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
*/
|
||||
interface Constants {
|
||||
|
||||
String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory";
|
||||
String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager";
|
||||
|
||||
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE1 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate";
|
||||
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE2 = "org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl";
|
||||
String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager";
|
||||
|
||||
// needed as Spring only exposes that interface via the EM proxy
|
||||
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.jpa.internal.PersistenceUnitUtilImpl";
|
||||
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor";
|
||||
|
||||
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
|
||||
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
|
||||
|
||||
}
|
||||
|
||||
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
|
||||
@@ -465,5 +523,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
|
||||
scrollableCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ package org.springframework.data.jpa.repository;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.LockModeType;
|
||||
@@ -28,6 +25,9 @@ import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2011-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jpa.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* {@code CurrentTenantIdentifierResolver} instance for testing
|
||||
*
|
||||
* @author Ariel Morelli Andres (Atlassian US, Inc.)
|
||||
*/
|
||||
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
|
||||
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
|
||||
|
||||
public static void setTenantIdentifier(String tenantIdentifier) {
|
||||
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
|
||||
}
|
||||
|
||||
public static void removeTenantIdentifier() {
|
||||
CURRENT_TENANT_IDENTIFIER.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveCurrentTenantIdentifier() {
|
||||
return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateExistingCurrentSessions() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2011-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jpa.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assumptions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
import org.springframework.data.jpa.domain.sample.Role;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.data.jpa.repository.sample.RoleRepository;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
/**
|
||||
* Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected
|
||||
* despite not having a tenant available at creation time
|
||||
*
|
||||
* @author Ariel Morelli Andres (Atlassian US, Inc.)
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration()
|
||||
class HibernateMultitenancyTests {
|
||||
|
||||
@Autowired RoleRepository roleRepository;
|
||||
@Autowired EntityManager em;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPersistenceProviderFromFactoryWithoutTenant() {
|
||||
PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
|
||||
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRepositoryWithTenant() {
|
||||
HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id");
|
||||
assertThatNoException().isThrownBy(() -> roleRepository.findAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRepositoryWithoutTenantFails() {
|
||||
assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
List<Role> insertAndQuery() {
|
||||
roleRepository.save(new Role("DRUMMER"));
|
||||
roleRepository.flush();
|
||||
return roleRepository.findAll();
|
||||
}
|
||||
|
||||
@ImportResource({ "classpath:multitenancy-test.xml" })
|
||||
@Configuration
|
||||
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
|
||||
includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
|
||||
static class TestConfig {}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -53,6 +54,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Ariel Morelli Andres
|
||||
*/
|
||||
class AbstractStringBasedJpaQueryUnitTests {
|
||||
|
||||
@@ -135,10 +137,12 @@ class AbstractStringBasedJpaQueryUnitTests {
|
||||
public EntityManager get() {
|
||||
|
||||
EntityManager em = Mockito.mock(EntityManager.class);
|
||||
EntityManagerFactory emf = Mockito.mock(EntityManagerFactory.class);
|
||||
|
||||
Metamodel meta = mock(Metamodel.class);
|
||||
when(em.getMetamodel()).thenReturn(meta);
|
||||
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
|
||||
when(em.getEntityManagerFactory()).thenReturn(emf);
|
||||
|
||||
return em;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
|
||||
@@ -60,6 +59,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* @author Jens Schauder
|
||||
* @author Greg Turnquist
|
||||
* @author Yanming Zhou
|
||||
* @author Ariel Morelli Andres (Atlassian US, Inc.)
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -84,6 +84,9 @@ class SimpleJpaRepositoryUnitTests {
|
||||
void setUp() {
|
||||
|
||||
when(em.getDelegate()).thenReturn(em);
|
||||
when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory);
|
||||
|
||||
when(entityManagerFactory.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil);
|
||||
|
||||
when(information.getJavaType()).thenReturn(User.class);
|
||||
when(em.getCriteriaBuilder()).thenReturn(builder);
|
||||
|
||||
40
spring-data-jpa/src/test/resources/multitenancy-test.xml
Normal file
40
spring-data-jpa/src/test/resources/multitenancy-test.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<import resource="hibernate.xml" />
|
||||
|
||||
<bean id="entityManagerFactory"
|
||||
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
|
||||
<property name="dataSource" ref="dataSource" />
|
||||
<property name="persistenceUnitName" value="spring-data-jpa" />
|
||||
<property name="jpaVendorAdapter" ref="vendorAdaptor" />
|
||||
<property name="jpaProperties">
|
||||
<props>
|
||||
<prop key="hibernate.tenant_identifier_resolver">
|
||||
org.springframework.data.jpa.repository.HibernateCurrentTenantIdentifierResolver
|
||||
</prop>
|
||||
</props>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="abstractVendorAdaptor" abstract="true">
|
||||
<property name="generateDdl" value="true" />
|
||||
<property name="database" value="HSQL" />
|
||||
</bean>
|
||||
|
||||
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
|
||||
<property name="entityManagerFactory" ref="entityManagerFactory" />
|
||||
</bean>
|
||||
|
||||
<bean name="sampleEvaluationContextExtension" class="org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension"/>
|
||||
|
||||
<jdbc:embedded-database id="dataSource" type="HSQL" generate-name="true">
|
||||
<jdbc:script execution="INIT" separator="/;" location="classpath:scripts/hsqldb-init.sql"/>
|
||||
<jdbc:script execution="INIT" separator="/;" location="classpath:scripts/schema-stored-procedures.sql"/>
|
||||
</jdbc:embedded-database>
|
||||
|
||||
</beans>
|
||||
Reference in New Issue
Block a user