diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java index f0cdd0a29cf..e1d0164ecaf 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java @@ -33,9 +33,12 @@ import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.integrator.spi.Integrator; import org.hibernate.service.ServiceRegistry; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -46,6 +49,7 @@ import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.filter.TypeFilter; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * {@link FactoryBean} that creates a Hibernate {@link SessionFactory}. This is the usual @@ -61,7 +65,7 @@ import org.springframework.lang.Nullable; * @see LocalSessionFactoryBuilder */ public class LocalSessionFactoryBean extends HibernateExceptionTranslator - implements FactoryBean, ResourceLoaderAware, InitializingBean, DisposableBean { + implements FactoryBean, ResourceLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { @Nullable private DataSource dataSource; @@ -131,6 +135,9 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator @Nullable private ResourcePatternResolver resourcePatternResolver; + @Nullable + private ConfigurableListableBeanFactory beanFactory; + @Nullable private Configuration configuration; @@ -433,6 +440,23 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator return this.resourcePatternResolver; } + /** + * Accept the containing {@link BeanFactory}, registering corresponding Hibernate + * {@link org.hibernate.resource.beans.container.spi.BeanContainer} integration for + * it if possible. This requires a Spring {@link ConfigurableListableBeanFactory} + * and Hibernate 5.3 or higher on the classpath. + * @since 5.1 + * @see LocalSessionFactoryBuilder#setBeanContainer + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableListableBeanFactory && + ClassUtils.isPresent("org.hibernate.resource.beans.container.spi.BeanContainer", + getClass().getClassLoader())) { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + } + @Override public void afterPropertiesSet() throws IOException { @@ -508,6 +532,10 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator sfb.setJtaTransactionManager(this.jtaTransactionManager); } + if (this.beanFactory != null) { + sfb.setBeanContainer(this.beanFactory); + } + if (this.multiTenantConnectionProvider != null) { sfb.setMultiTenantConnectionProvider(this.multiTenantConnectionProvider); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java index 7ea1c4573e6..4d8062fbb22 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java @@ -46,6 +46,7 @@ import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.InfrastructureProxy; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -228,6 +229,19 @@ public class LocalSessionFactoryBuilder extends Configuration { return this; } + /** + * Set a Hibernate {@link org.hibernate.resource.beans.container.spi.BeanContainer} + * for the given Spring {@link ConfigurableListableBeanFactory}. + *

Note: Bean container integration requires Hibernate 5.3 or higher. + * It enables autowiring of Hibernate attribute converters and entity listeners. + * @since 5.1 + * @see AvailableSettings#BEAN_CONTAINER + */ + public LocalSessionFactoryBuilder setBeanContainer(ConfigurableListableBeanFactory beanFactory) { + getProperties().put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory)); + return this; + } + /** * Set a {@link MultiTenantConnectionProvider} to be passed on to the SessionFactory. * @since 4.3 diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java new file mode 100644 index 00000000000..499d3dab771 --- /dev/null +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.hibernate5; + +import java.util.Map; +import java.util.function.Consumer; + +import org.hibernate.resource.beans.container.spi.BeanContainer; +import org.hibernate.resource.beans.container.spi.ContainedBean; +import org.hibernate.resource.beans.spi.BeanInstanceProducer; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Spring's implementation of Hibernate 5.3's {@link BeanContainer} SPI, + * delegating to a Spring {@link ConfigurableListableBeanFactory}. + * + * @author Juergen Hoeller + * @since 5.1 + */ +final class SpringBeanContainer implements BeanContainer { + + private final ConfigurableListableBeanFactory beanFactory; + + private final Map> beanCache = new ConcurrentReferenceHashMap<>(); + + + public SpringBeanContainer(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + @SuppressWarnings("unchecked") + public ContainedBean getBean( + Class beanType, LifecycleOptions lifecycleOptions, BeanInstanceProducer fallbackProducer) { + + SpringContainedBean bean; + if (lifecycleOptions.canUseCachedReferences()) { + bean = this.beanCache.get(beanType); + if (bean == null) { + bean = createBean(beanType, lifecycleOptions); + this.beanCache.put(beanType, bean); + } + } + else { + bean = createBean(beanType, lifecycleOptions); + } + return (SpringContainedBean) bean; + } + + @Override + @SuppressWarnings("unchecked") + public ContainedBean getBean( + String name, Class beanType, LifecycleOptions lifecycleOptions, BeanInstanceProducer fallbackProducer) { + + if (!this.beanFactory.containsBean(name)) { + return getBean(beanType, lifecycleOptions, fallbackProducer); + } + + SpringContainedBean bean; + if (lifecycleOptions.canUseCachedReferences()) { + bean = this.beanCache.get(name); + if (bean == null) { + bean = createBean(name, beanType, lifecycleOptions); + this.beanCache.put(name, bean); + } + } + else { + bean = createBean(name, beanType, lifecycleOptions); + } + return (SpringContainedBean) bean; + } + + @Override + public void stop() { + this.beanCache.values().forEach(SpringContainedBean::destroyIfNecessary); + this.beanCache.clear(); + } + + + private SpringContainedBean createBean(Class beanType, LifecycleOptions lifecycleOptions) { + if (lifecycleOptions.useJpaCompliantCreation()) { + return new SpringContainedBean<>( + this.beanFactory.createBean(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false), + this.beanFactory::destroyBean); + } + else { + return new SpringContainedBean<>(this.beanFactory.getBean(beanType)); + } + } + + private SpringContainedBean createBean(String name, Class beanType, LifecycleOptions lifecycleOptions) { + if (lifecycleOptions.useJpaCompliantCreation()) { + Object bean = this.beanFactory.autowire(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false); + this.beanFactory.applyBeanPropertyValues(bean, name); + this.beanFactory.initializeBean(bean, name); + return new SpringContainedBean<>(bean, beanInstance -> this.beanFactory.destroyBean(name, beanInstance)); + } + else { + return new SpringContainedBean<>(this.beanFactory.getBean(name, beanType)); + } + } + + + private static final class SpringContainedBean implements ContainedBean { + + private final B beanInstance; + + @Nullable + private Consumer destructionCallback; + + public SpringContainedBean(B beanInstance) { + this.beanInstance = beanInstance; + } + + public SpringContainedBean(B beanInstance, Consumer destructionCallback) { + this.beanInstance = beanInstance; + this.destructionCallback = destructionCallback; + } + + @Override + public B getBeanInstance() { + return this.beanInstance; + } + + public void destroyIfNecessary() { + if (this.destructionCallback != null) { + this.destructionCallback.accept(this.beanInstance); + } + } + } + +} diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Person.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Person.java index 27dd1c8766e..0e165ec0a30 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Person.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2018 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. @@ -19,6 +19,7 @@ package org.springframework.orm.jpa.domain; import javax.persistence.Basic; import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -26,6 +27,7 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; +import org.springframework.context.ApplicationContext; import org.springframework.tests.sample.beans.TestBean; /** @@ -34,6 +36,7 @@ import org.springframework.tests.sample.beans.TestBean; * @author Rod Johnson */ @Entity +@EntityListeners(PersonListener.class) public class Person { @Id @@ -52,6 +55,8 @@ public class Person { @Basic(fetch = FetchType.LAZY) private String last_name; + public transient ApplicationContext postLoaded; + public Integer getId() { return id; @@ -91,8 +96,8 @@ public class Person { @Override public String toString() { - return getClass().getName() + ":(" + hashCode() + ") id=" + id + "; firstName=" + first_name + "; lastName=" - + last_name + "; testBean=" + testBean; + return getClass().getName() + ":(" + hashCode() + ") id=" + id + "; firstName=" + first_name + + "; lastName=" + last_name + "; testBean=" + testBean; } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain/PersonListener.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/PersonListener.java new file mode 100644 index 00000000000..17770e77ba2 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain/PersonListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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 + * + * http://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 javax.persistence.PostLoad; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +/** + * @author Juergen Hoeller + */ +public class PersonListener { + + @Autowired + ApplicationContext context; + + @PostLoad + public void postLoad(Person person) { + person.postLoaded = this.context; + } + +} diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java index b623363f223..796448bc424 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java @@ -23,6 +23,7 @@ import org.hibernate.query.Query; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.orm.jpa.AbstractContainerEntityManagerFactoryIntegrationTests; import org.springframework.orm.jpa.EntityManagerFactoryInfo; import org.springframework.orm.jpa.domain.Person; @@ -40,6 +41,9 @@ public class HibernateNativeEntityManagerFactoryIntegrationTests extends Abstrac @Autowired private SessionFactory sessionFactory; + @Autowired + private ApplicationContext applicationContext; + @Override protected String[] getConfigLocations() { @@ -53,18 +57,29 @@ public class HibernateNativeEntityManagerFactoryIntegrationTests extends Abstrac assertFalse("Must not have introduced config interface", entityManagerFactory instanceof EntityManagerFactoryInfo); } + @Test + @SuppressWarnings("unchecked") + public void testEntityListener() { + String firstName = "Tony"; + insertPerson(firstName); + + List people = sharedEntityManager.createQuery("select p from Person as p").getResultList(); + assertEquals(1, people.size()); + assertEquals(firstName, people.get(0).getFirstName()); + assertSame(applicationContext, people.get(0).postLoaded); + } + @Test @SuppressWarnings("unchecked") public void testCurrentSession() { - // Add with JDBC String firstName = "Tony"; insertPerson(firstName); Query q = sessionFactory.getCurrentSession().createQuery("select p from Person as p"); List people = q.getResultList(); - assertEquals(1, people.size()); assertEquals(firstName, people.get(0).getFirstName()); + assertSame(applicationContext, people.get(0).postLoaded); } }