Support for qualified EntityManager/EntityManagerFactory injection (JPA 3.2)

Closes gh-33414
This commit is contained in:
Juergen Hoeller 2025-06-24 18:51:18 +02:00
parent b0eacd22e0
commit 04f3975e0f
11 changed files with 201 additions and 72 deletions

View File

@ -53,8 +53,8 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.SmartFactoryBean;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.DataAccessException;
@ -66,7 +66,9 @@ import org.springframework.util.CollectionUtils;
/**
* Abstract {@link org.springframework.beans.factory.FactoryBean} that creates
* a local JPA {@link jakarta.persistence.EntityManagerFactory} instance within
* a Spring application context.
* a Spring application context. As of 7.0, it additionally exposes a shared
* {@link jakarta.persistence.EntityManager} instance through {@link SmartFactoryBean},
* making {@code EntityManager} available for dependency injection as well.
*
* <p>Encapsulates the common functionality between the different JPA bootstrap
* contracts (standalone as well as container).
@ -91,7 +93,7 @@ import org.springframework.util.CollectionUtils;
*/
@SuppressWarnings("serial")
public abstract class AbstractEntityManagerFactoryBean implements
FactoryBean<EntityManagerFactory>, BeanClassLoaderAware, BeanFactoryAware,
SmartFactoryBean<EntityManagerFactory>, BeanClassLoaderAware, BeanFactoryAware,
BeanNameAware, InitializingBean, SmartInitializingSingleton, DisposableBean,
EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable {
@ -131,6 +133,9 @@ public abstract class AbstractEntityManagerFactoryBean implements
/** Exposed client-level EntityManagerFactory proxy. */
private @Nullable EntityManagerFactory entityManagerFactory;
/** Exposed client-level shared EntityManager proxy. */
private @Nullable EntityManager sharedEntityManager;
/**
* Set the PersistenceProvider implementation class to use for creating the
@ -333,11 +338,19 @@ public abstract class AbstractEntityManagerFactoryBean implements
this.beanFactory = beanFactory;
}
protected @Nullable BeanFactory getBeanFactory() {
return this.beanFactory;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
protected @Nullable String getBeanName() {
return this.beanName;
}
@Override
public void afterPropertiesSet() throws PersistenceException {
@ -386,6 +399,7 @@ public abstract class AbstractEntityManagerFactoryBean implements
// application-managed EntityManager proxy that automatically joins
// existing transactions.
this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory);
this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory);
}
@Override
@ -621,9 +635,23 @@ public abstract class AbstractEntityManagerFactoryBean implements
return (this.entityManagerFactory != null ? this.entityManagerFactory.getClass() : EntityManagerFactory.class);
}
/**
* Return either the singleton EntityManagerFactory or the shared EntityManager proxy.
*/
@Override
public boolean isSingleton() {
return true;
public <S> @Nullable S getObject(Class<S> type) throws Exception {
if (EntityManager.class.isAssignableFrom(type)) {
return (type.isInstance(this.sharedEntityManager) ? type.cast(this.sharedEntityManager) : null);
}
return SmartFactoryBean.super.getObject(type);
}
@Override
public boolean supportsType(Class<?> type) {
if (EntityManager.class.isAssignableFrom(type)) {
return type.isInstance(this.sharedEntityManager);
}
return SmartFactoryBean.super.supportsType(type);
}

View File

@ -16,6 +16,8 @@
package org.springframework.orm.jpa;
import java.util.List;
import javax.sql.DataSource;
import jakarta.persistence.EntityManagerFactory;
@ -27,6 +29,11 @@ import jakarta.persistence.spi.PersistenceUnitInfo;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.weaving.LoadTimeWeaverAware;
import org.springframework.core.io.ResourceLoader;
@ -40,6 +47,8 @@ import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link org.springframework.beans.factory.FactoryBean} that creates a JPA
@ -361,6 +370,25 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
}
}
String scope = this.persistenceUnitInfo.getScopeAnnotationName();
if (StringUtils.hasText(scope)) {
logger.info("Scope annotation name for persistence unit ignored by Spring: " + scope);
}
List<String> qualifiers = this.persistenceUnitInfo.getQualifierAnnotationNames();
if (!CollectionUtils.isEmpty(qualifiers)) {
BeanFactory beanFactory = getBeanFactory();
String beanName = getBeanName();
if (beanFactory instanceof ConfigurableBeanFactory cbf && beanName != null) {
BeanDefinition bd = cbf.getMergedBeanDefinition(beanName);
if (bd instanceof AbstractBeanDefinition abd) {
for (String qualifier : qualifiers) {
abd.addQualifier(new AutowireCandidateQualifier(qualifier));
}
}
}
}
super.afterPropertiesSet();
}

View File

@ -52,6 +52,10 @@ public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo {
private @Nullable String persistenceProviderClassName;
private @Nullable String scopeAnnotationName;
private final List<String> qualifierAnnotationNames = new ArrayList<>();
private @Nullable PersistenceUnitTransactionType transactionType;
private @Nullable DataSource nonJtaDataSource;
@ -76,7 +80,7 @@ public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo {
private Properties properties = new Properties();
private String persistenceXMLSchemaVersion = "2.0";
private String persistenceXMLSchemaVersion = "3.2";
private @Nullable String persistenceProviderPackageName;
@ -99,6 +103,24 @@ public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo {
return this.persistenceProviderClassName;
}
public void setScopeAnnotationName(@Nullable String scopeAnnotationName) {
this.scopeAnnotationName = scopeAnnotationName;
}
@Override
public @Nullable String getScopeAnnotationName() {
return this.scopeAnnotationName;
}
public void addQualifierAnnotationName(String qualifierAnnotationName) {
this.qualifierAnnotationNames.add(qualifierAnnotationName);
}
@Override
public List<String> getQualifierAnnotationNames() {
return this.qualifierAnnotationNames;
}
public void setTransactionType(PersistenceUnitTransactionType transactionType) {
this.transactionType = transactionType;
}
@ -276,16 +298,6 @@ public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo {
throw new UnsupportedOperationException("getNewTempClassLoader not supported");
}
@Override
public @Nullable String getScopeAnnotationName() {
return null;
}
@Override
public @Nullable List<String> getQualifierAnnotationNames() {
return null;
}
@Override
public String toString() {

View File

@ -61,28 +61,32 @@ final class PersistenceUnitReader {
private static final String UNIT_NAME = "name";
private static final String MAPPING_FILE_NAME = "mapping-file";
private static final String JAR_FILE_URL = "jar-file";
private static final String MANAGED_CLASS_NAME = "class";
private static final String PROPERTIES = "properties";
private static final String PROVIDER = "provider";
private static final String SCOPE = "scope";
private static final String QUALIFIER = "qualifier";
private static final String TRANSACTION_TYPE = "transaction-type";
private static final String JTA_DATA_SOURCE = "jta-data-source";
private static final String NON_JTA_DATA_SOURCE = "non-jta-data-source";
private static final String MAPPING_FILE_NAME = "mapping-file";
private static final String JAR_FILE_URL = "jar-file";
private static final String MANAGED_CLASS_NAME = "class";
private static final String EXCLUDE_UNLISTED_CLASSES = "exclude-unlisted-classes";
private static final String SHARED_CACHE_MODE = "shared-cache-mode";
private static final String VALIDATION_MODE = "validation-mode";
private static final String PROPERTIES = "properties";
private static final String META_INF = "META-INF";
@ -200,6 +204,18 @@ final class PersistenceUnitReader {
// set unit name
unitInfo.setPersistenceUnitName(persistenceUnit.getAttribute(UNIT_NAME).trim());
// provider
String provider = DomUtils.getChildElementValueByTagName(persistenceUnit, PROVIDER);
if (StringUtils.hasText(provider)) {
unitInfo.setPersistenceProviderClassName(provider.trim());
}
// scope
String scope = DomUtils.getChildElementValueByTagName(persistenceUnit, SCOPE);
if (StringUtils.hasText(scope)) {
unitInfo.setScopeAnnotationName(scope.trim());
}
// set transaction type
String txType = persistenceUnit.getAttribute(TRANSACTION_TYPE).trim();
if (StringUtils.hasText(txType)) {
@ -217,12 +233,6 @@ final class PersistenceUnitReader {
unitInfo.setNonJtaDataSource(this.dataSourceLookup.getDataSource(nonJtaDataSource.trim()));
}
// provider
String provider = DomUtils.getChildElementValueByTagName(persistenceUnit, PROVIDER);
if (StringUtils.hasText(provider)) {
unitInfo.setPersistenceProviderClassName(provider.trim());
}
// exclude unlisted classes
Element excludeUnlistedClasses = DomUtils.getChildElementByTagName(persistenceUnit, EXCLUDE_UNLISTED_CLASSES);
if (excludeUnlistedClasses != null) {
@ -242,39 +252,24 @@ final class PersistenceUnitReader {
unitInfo.setValidationMode(ValidationMode.valueOf(validationMode));
}
parseProperties(persistenceUnit, unitInfo);
parseManagedClasses(persistenceUnit, unitInfo);
parseQualifiers(persistenceUnit, unitInfo);
parseMappingFiles(persistenceUnit, unitInfo);
parseJarFiles(persistenceUnit, unitInfo);
parseManagedClasses(persistenceUnit, unitInfo);
parseProperties(persistenceUnit, unitInfo);
return unitInfo;
}
/**
* Parse the {@code property} XML elements.
* Parse the {@code qualifier} XML elements.
*/
void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES);
if (propRoot == null) {
return;
}
List<Element> properties = DomUtils.getChildElementsByTagName(propRoot, "property");
for (Element property : properties) {
String name = property.getAttribute("name");
String value = property.getAttribute("value");
unitInfo.addProperty(name, value);
}
}
/**
* Parse the {@code class} XML elements.
*/
void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
List<Element> classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME);
void parseQualifiers(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
List<Element> classes = DomUtils.getChildElementsByTagName(persistenceUnit, QUALIFIER);
for (Element element : classes) {
String value = DomUtils.getTextValue(element).trim();
if (StringUtils.hasText(value)) {
unitInfo.addManagedClassName(value);
unitInfo.addQualifierAnnotationName(value);
}
}
}
@ -323,6 +318,35 @@ final class PersistenceUnitReader {
}
}
/**
* Parse the {@code class} XML elements.
*/
void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
List<Element> classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME);
for (Element element : classes) {
String value = DomUtils.getTextValue(element).trim();
if (StringUtils.hasText(value)) {
unitInfo.addManagedClassName(value);
}
}
}
/**
* Parse the {@code property} XML elements.
*/
void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES);
if (propRoot == null) {
return;
}
List<Element> properties = DomUtils.getChildElementsByTagName(propRoot, "property");
for (Element property : properties) {
String name = property.getAttribute("name");
String value = property.getAttribute("value");
unitInfo.addProperty(name, value);
}
}
/**
* Determine the persistence unit root URL based on the given resource

View File

@ -32,6 +32,7 @@ import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.orm.jpa.domain.MyDomain;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
@ -70,10 +71,14 @@ public abstract class AbstractEntityManagerFactoryIntegrationTests {
private boolean zappedTables = false;
@Autowired
@Autowired @MyDomain
public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
this.sharedEntityManager = SharedEntityManagerCreator.createSharedEntityManager(this.entityManagerFactory);
}
@Autowired @MyDomain
public void setSharedEntityManager(EntityManager sharedEntityManager) {
this.sharedEntityManager = sharedEntityManager;
}
@Autowired

View File

@ -0,0 +1,31 @@
/*
* Copyright 2002-present 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.orm.jpa.domain;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyDomain {
}

View File

@ -1,11 +1,10 @@
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.2">
<persistence-unit name="Person" transaction-type="RESOURCE_LOCAL">
<class>org.springframework.orm.jpa.domain.DriversLicense</class>
<class>org.springframework.orm.jpa.domain.Person</class>
<exclude-unlisted-classes />
<exclude-unlisted-classes/>
<qualifier>org.springframework.orm.jpa.domain.MyDomain</qualifier>
</persistence-unit>
</persistence>

View File

@ -1,11 +1,10 @@
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.2">
<persistence-unit name="Person" transaction-type="RESOURCE_LOCAL">
<class>org.springframework.orm.jpa.domain.Person</class>
<class>org.springframework.orm.jpa.domain.DriversLicense</class>
<exclude-unlisted-classes />
<class>org.springframework.orm.jpa.domain.Person</class>
<exclude-unlisted-classes/>
<qualifier>org.springframework.orm.jpa.domain.MyDomain</qualifier>
</persistence-unit>
</persistence>

View File

@ -18,6 +18,12 @@
<prop key="hibernate.cache.provider_class">org.hibernate.cache.HashtableCacheProvider</prop>
</props>
</property>
<qualifier type="org.springframework.orm.jpa.domain.MyDomain"/>
</bean>
<bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<qualifier type="org.springframework.orm.jpa.domain.MyDomain"/>
</bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">

View File

@ -3,10 +3,6 @@
<beans>
<bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:xdb"/>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="persistenceUnitManager" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
<property name="persistenceXmlLocations" value="org/springframework/orm/jpa/domain/persistence-multi.xml"/>
@ -16,6 +16,7 @@
<bean id="entityManagerFactory" parent="abstractEMF">
<property name="persistenceUnitName" value="Drivers"/>
<qualifier type="org.springframework.orm.jpa.domain.MyDomain"/>
</bean>
<bean id="entityManagerFactory2" parent="abstractEMF">