From db1171d5c4839293aa390e301af73b100fc206e7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 19 Dec 2015 15:13:46 +0100 Subject: [PATCH] Background bootstrapping via "bootstrapExecutor" for LocalContainerEntityManagerFactoryBean and LocalSessionFactoryBean/Builder Issue: SPR-13732 --- .../hibernate5/LocalSessionFactoryBean.java | 23 ++++- .../LocalSessionFactoryBuilder.java | 85 ++++++++++++++++ .../orm/hibernate5/SessionFactoryUtils.java | 10 +- .../jpa/AbstractEntityManagerFactoryBean.java | 97 ++++++++++++++++--- ...ocalContainerEntityManagerFactoryBean.java | 17 ++-- .../orm/jpa/hibernate/hibernate-manager.xml | 13 ++- 6 files changed, 212 insertions(+), 33 deletions(-) diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java index bf613b025226..b4d2d941abf9 100644 --- a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java @@ -38,6 +38,7 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.filter.TypeFilter; /** @@ -93,6 +94,8 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator private String[] packagesToScan; + private AsyncTaskExecutor bootstrapExecutor; + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); private Configuration configuration; @@ -298,6 +301,23 @@ public void setPackagesToScan(String... packagesToScan) { this.packagesToScan = packagesToScan; } + /** + * Specify an asynchronous executor for background bootstrapping, + * e.g. a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}. + *

{@code SessionFactory} initialization will then switch into background + * bootstrap mode, with a {@code SessionFactory} proxy immediately returned for + * injection purposes instead of waiting for Hibernate's bootstrapping to complete. + * However, note that the first actual call to a {@code SessionFactory} method will + * then block until Hibernate's bootstrapping completed, if not ready by then. + * For maximum benefit, make sure to avoid early {@code SessionFactory} calls + * in init methods of related beans, even for metadata introspection purposes. + * @see LocalSessionFactoryBuilder#buildSessionFactory(AsyncTaskExecutor) + * @since 4.3 + */ + public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { + this.bootstrapExecutor = bootstrapExecutor; + } + @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); @@ -413,7 +433,8 @@ public void afterPropertiesSet() throws IOException { * @see LocalSessionFactoryBuilder#buildSessionFactory */ protected SessionFactory buildSessionFactory(LocalSessionFactoryBuilder sfb) { - return sfb.buildSessionFactory(); + return (this.bootstrapExecutor != null ? sfb.buildSessionFactory(this.bootstrapExecutor) : + sfb.buildSessionFactory()); } /** diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java index def7931f09b8..2b83c42b2feb 100644 --- a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java @@ -17,9 +17,16 @@ package org.springframework.orm.hibernate5; import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.Collections; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import javax.persistence.AttributeConverter; import javax.persistence.Converter; import javax.persistence.Embeddable; @@ -30,15 +37,18 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; +import org.hibernate.SessionFactory; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Environment; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; @@ -263,4 +273,79 @@ private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFac return false; } + /** + * Build the Hibernate {@code SessionFactory} through background bootstrapping, + * using the given executor for a parallel initialization phase + * (e.g. a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}). + *

{@code SessionFactory} initialization will then switch into background + * bootstrap mode, with a {@code SessionFactory} proxy immediately returned for + * injection purposes instead of waiting for Hibernate's bootstrapping to complete. + * However, note that the first actual call to a {@code SessionFactory} method will + * then block until Hibernate's bootstrapping completed, if not ready by then. + * For maximum benefit, make sure to avoid early {@code SessionFactory} calls + * in init methods of related beans, even for metadata introspection purposes. + * @since 4.3 + * @see #buildSessionFactory() + */ + public SessionFactory buildSessionFactory(AsyncTaskExecutor bootstrapExecutor) { + Assert.notNull(bootstrapExecutor, "AsyncTaskExecutor must not be null"); + return (SessionFactory) Proxy.newProxyInstance(this.resourcePatternResolver.getClassLoader(), + new Class[] {SessionFactoryImplementor.class}, + new BootstrapSessionFactoryInvocationHandler(bootstrapExecutor)); + } + + + /** + * Proxy invocation handler for background bootstrapping, only enforcing + * a fully initialized target {@code SessionFactory} when actually needed. + */ + private class BootstrapSessionFactoryInvocationHandler implements InvocationHandler { + + private final Future sessionFactoryFuture; + + public BootstrapSessionFactoryInvocationHandler(AsyncTaskExecutor bootstrapExecutor) { + this.sessionFactoryFuture = bootstrapExecutor.submit(new Callable() { + @Override + public SessionFactory call() throws Exception { + return buildSessionFactory(); + } + }); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of EntityManagerFactory proxy. + return System.identityHashCode(proxy); + } + else if (method.getName().equals("getProperties")) { + return getProperties(); + } + // Regular delegation to the target SessionFactory, + // enforcing its full initialization... + return method.invoke(getSessionFactory(), args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + private SessionFactory getSessionFactory() { + try { + return this.sessionFactoryFuture.get(); + } + catch (InterruptedException ex) { + throw new IllegalStateException("Interrupted during initialization of Hibernate SessionFactory", ex); + } + catch (ExecutionException ex) { + throw new IllegalStateException("Failed to asynchronously initialize Hibernate SessionFactory", ex); + } + } + } + } diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java index 175dd05b8e20..19151579b898 100644 --- a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java @@ -20,7 +20,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.hibernate.HibernateException; import org.hibernate.JDBCException; import org.hibernate.NonUniqueObjectException; @@ -38,6 +37,7 @@ import org.hibernate.TransientObjectException; import org.hibernate.UnresolvableObjectException; import org.hibernate.WrongClassException; +import org.hibernate.cfg.Environment; import org.hibernate.dialect.lock.OptimisticEntityLockException; import org.hibernate.dialect.lock.PessimisticEntityLockException; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; @@ -94,9 +94,13 @@ public abstract class SessionFactoryUtils { */ public static DataSource getDataSource(SessionFactory sessionFactory) { if (sessionFactory instanceof SessionFactoryImplementor) { + SessionFactoryImplementor sfi = (SessionFactoryImplementor) sessionFactory; + Object dataSourceValue = sfi.getProperties().get(Environment.DATASOURCE); + if (dataSourceValue instanceof DataSource) { + return (DataSource) dataSourceValue; + } try { - ConnectionProvider cp = ((SessionFactoryImplementor) sessionFactory).getServiceRegistry().getService( - ConnectionProvider.class); + ConnectionProvider cp = sfi.getServiceRegistry().getService(ConnectionProvider.class); if (cp != null) { return cp.unwrap(DataSource.class); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index 872ba91f37bc..1bc736179b5f 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -30,6 +30,9 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceException; @@ -48,6 +51,7 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.util.Assert; @@ -102,6 +106,8 @@ public abstract class AbstractEntityManagerFactoryBean implements private JpaVendorAdapter jpaVendorAdapter; + private AsyncTaskExecutor bootstrapExecutor; + private ClassLoader beanClassLoader = getClass().getClassLoader(); private BeanFactory beanFactory; @@ -109,8 +115,12 @@ public abstract class AbstractEntityManagerFactoryBean implements private String beanName; /** Raw EntityManagerFactory as returned by the PersistenceProvider */ - public EntityManagerFactory nativeEntityManagerFactory; + private EntityManagerFactory nativeEntityManagerFactory; + + /** Future for lazily initializing raw target EntityManagerFactory */ + private Future nativeEntityManagerFactoryFuture; + /** Exposed client-level EntityManagerFactory proxy */ private EntityManagerFactory entityManagerFactory; @@ -263,6 +273,30 @@ public JpaVendorAdapter getJpaVendorAdapter() { return this.jpaVendorAdapter; } + /** + * Specify an asynchronous executor for background bootstrapping, + * e.g. a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}. + *

{@code EntityManagerFactory} initialization will then switch into background + * bootstrap mode, with a {@code EntityManagerFactory} proxy immediately returned for + * injection purposes instead of waiting for the JPA provider's bootstrapping to complete. + * However, note that the first actual call to a {@code EntityManagerFactory} method will + * then block until the JPA provider's bootstrapping completed, if not ready by then. + * For maximum benefit, make sure to avoid early {@code EntityManagerFactory} calls + * in init methods of related beans, even for metadata introspection purposes. + * @since 4.3 + */ + public void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor) { + this.bootstrapExecutor = bootstrapExecutor; + } + + /** + * Return the asynchronous executor for background bootstrapping, if any. + * @since 4.3 + */ + public AsyncTaskExecutor getBootstrapExecutor() { + return this.bootstrapExecutor; + } + @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; @@ -315,13 +349,16 @@ public final void afterPropertiesSet() throws PersistenceException { } } - this.nativeEntityManagerFactory = createNativeEntityManagerFactory(); - if (this.nativeEntityManagerFactory == null) { - throw new IllegalStateException( - "JPA PersistenceProvider returned null EntityManagerFactory - check your JPA provider setup!"); + if (this.bootstrapExecutor != null) { + this.nativeEntityManagerFactoryFuture = this.bootstrapExecutor.submit(new Callable() { + @Override + public EntityManagerFactory call() { + return buildNativeEntityManagerFactory(); + } + }); } - if (this.jpaVendorAdapter != null) { - this.jpaVendorAdapter.postProcessEntityManagerFactory(this.nativeEntityManagerFactory); + else { + this.nativeEntityManagerFactory = buildNativeEntityManagerFactory(); } // Wrap the EntityManagerFactory in a factory implementing all its interfaces. @@ -329,6 +366,23 @@ public final void afterPropertiesSet() throws PersistenceException { // application-managed EntityManager proxy that automatically joins // existing transactions. this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory); + System.out.println("Returning: " + System.currentTimeMillis()); + } + + private EntityManagerFactory buildNativeEntityManagerFactory() { + EntityManagerFactory emf = createNativeEntityManagerFactory(); + if (emf == null) { + throw new IllegalStateException( + "JPA PersistenceProvider returned null EntityManagerFactory - check your JPA provider setup!"); + } + if (this.jpaVendorAdapter != null) { + this.jpaVendorAdapter.postProcessEntityManagerFactory(emf); + } + if (logger.isInfoEnabled()) { + logger.info("Initialized JPA EntityManagerFactory for persistence unit '" + getPersistenceUnitName() + "'"); + } + System.out.println("Done: " + System.currentTimeMillis()); + return emf; } /** @@ -343,9 +397,12 @@ protected EntityManagerFactory createEntityManagerFactoryProxy(EntityManagerFact if (this.entityManagerFactoryInterface != null) { ifcs.add(this.entityManagerFactoryInterface); } - else { + else if (emf != null) { ifcs.addAll(ClassUtils.getAllInterfacesForClassAsSet(emf.getClass(), this.beanClassLoader)); } + else { + ifcs.add(EntityManagerFactory.class); + } ifcs.add(EntityManagerFactoryInfo.class); try { return (EntityManagerFactory) Proxy.newProxyInstance( @@ -379,13 +436,13 @@ else if (method.getName().equals("createEntityManager") && args != null && args. // JPA 2.1's createEntityManager(SynchronizationType, Map) // Redirect to plain createEntityManager and add synchronization semantics through Spring proxy EntityManager rawEntityManager = (args.length > 1 ? - this.nativeEntityManagerFactory.createEntityManager((Map) args[1]) : - this.nativeEntityManagerFactory.createEntityManager()); + getNativeEntityManagerFactory().createEntityManager((Map) args[1]) : + getNativeEntityManagerFactory().createEntityManager()); return ExtendedEntityManagerCreator.createApplicationManagedEntityManager(rawEntityManager, this, true); } // Standard delegation to the native factory, just post-processing EntityManager return values - Object retVal = method.invoke(this.nativeEntityManagerFactory, args); + Object retVal = method.invoke(getNativeEntityManagerFactory(), args); if (retVal instanceof EntityManager) { // Any other createEntityManager variant - expecting non-synchronized semantics EntityManager rawEntityManager = (EntityManager) retVal; @@ -420,7 +477,21 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { @Override public EntityManagerFactory getNativeEntityManagerFactory() { - return this.nativeEntityManagerFactory; + if (this.nativeEntityManagerFactory != null) { + return this.nativeEntityManagerFactory; + } + else { + System.out.println("Requested: " + System.currentTimeMillis()); + try { + return this.nativeEntityManagerFactoryFuture.get(); + } + catch (InterruptedException ex) { + throw new IllegalStateException("Interrupted during initialization of native EntityManagerFactory", ex); + } + catch (ExecutionException ex) { + throw new IllegalStateException("Failed to asynchronously initialize native EntityManagerFactory", ex); + } + } } @Override diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index e9fbb6ce540d..8cfd8e99d289 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -43,8 +43,8 @@ * up a shared JPA EntityManagerFactory in a Spring application context; * the EntityManagerFactory can then be passed to JPA-based DAOs via * dependency injection. Note that switching to a JNDI lookup or to a - * {@link LocalEntityManagerFactoryBean} - * definition is just a matter of configuration! + * {@link LocalEntityManagerFactoryBean} definition is just a matter of + * configuration! * *

As with {@link LocalEntityManagerFactoryBean}, configuration settings * are usually read in from a {@code META-INF/persistence.xml} config file, @@ -329,21 +329,16 @@ protected EntityManagerFactory createNativeEntityManagerFactory() throws Persist Class providerClass = ClassUtils.resolveClassName(providerClassName, getBeanClassLoader()); provider = (PersistenceProvider) BeanUtils.instantiateClass(providerClass); } - if (provider == null) { - throw new IllegalStateException("Unable to determine persistence provider. " + - "Please check configuration of " + getClass().getName() + "; " + - "ideally specify the appropriate JpaVendorAdapter class for this provider."); - } if (logger.isInfoEnabled()) { logger.info("Building JPA container EntityManagerFactory for persistence unit '" + this.persistenceUnitInfo.getPersistenceUnitName() + "'"); } - this.nativeEntityManagerFactory = + EntityManagerFactory emf = provider.createContainerEntityManagerFactory(this.persistenceUnitInfo, getJpaPropertyMap()); - postProcessEntityManagerFactory(this.nativeEntityManagerFactory, this.persistenceUnitInfo); + postProcessEntityManagerFactory(emf, this.persistenceUnitInfo); - return this.nativeEntityManagerFactory; + return emf; } diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml index 235019071133..ea7ce16c84b9 100644 --- a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager.xml @@ -10,13 +10,13 @@ - - + + - - - + + + @@ -25,6 +25,9 @@ + + +