001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.orm.hibernate5; 018 019import java.io.IOException; 020import java.lang.reflect.InvocationHandler; 021import java.lang.reflect.InvocationTargetException; 022import java.lang.reflect.Method; 023import java.lang.reflect.Proxy; 024import java.util.Collections; 025import java.util.Set; 026import java.util.TreeSet; 027import java.util.concurrent.Callable; 028import java.util.concurrent.ExecutionException; 029import java.util.concurrent.Future; 030import javax.persistence.AttributeConverter; 031import javax.persistence.Converter; 032import javax.persistence.Embeddable; 033import javax.persistence.Entity; 034import javax.persistence.MappedSuperclass; 035import javax.sql.DataSource; 036import javax.transaction.TransactionManager; 037 038import org.hibernate.HibernateException; 039import org.hibernate.MappingException; 040import org.hibernate.SessionFactory; 041import org.hibernate.boot.MetadataSources; 042import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; 043import org.hibernate.cfg.AvailableSettings; 044import org.hibernate.cfg.Configuration; 045import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 046import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; 047import org.hibernate.engine.spi.SessionFactoryImplementor; 048 049import org.springframework.core.InfrastructureProxy; 050import org.springframework.core.io.Resource; 051import org.springframework.core.io.ResourceLoader; 052import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 053import org.springframework.core.io.support.ResourcePatternResolver; 054import org.springframework.core.io.support.ResourcePatternUtils; 055import org.springframework.core.task.AsyncTaskExecutor; 056import org.springframework.core.type.classreading.CachingMetadataReaderFactory; 057import org.springframework.core.type.classreading.MetadataReader; 058import org.springframework.core.type.classreading.MetadataReaderFactory; 059import org.springframework.core.type.filter.AnnotationTypeFilter; 060import org.springframework.core.type.filter.TypeFilter; 061import org.springframework.transaction.jta.JtaTransactionManager; 062import org.springframework.util.Assert; 063import org.springframework.util.ClassUtils; 064 065/** 066 * A Spring-provided extension of the standard Hibernate {@link Configuration} class, 067 * adding {@link SpringSessionContext} as a default and providing convenient ways 068 * to specify a DataSource and an application class loader. 069 * 070 * <p>This is designed for programmatic use, e.g. in {@code @Bean} factory methods. 071 * Consider using {@link LocalSessionFactoryBean} for XML bean definition files. 072 * 073 * <p>Compatible with Hibernate 5.0/5.1 as well as 5.2, as of Spring 4.3. 074 * 075 * @author Juergen Hoeller 076 * @since 4.2 077 * @see LocalSessionFactoryBean 078 */ 079@SuppressWarnings("serial") 080public class LocalSessionFactoryBuilder extends Configuration { 081 082 private static final String RESOURCE_PATTERN = "/**/*.class"; 083 084 private static final String PACKAGE_INFO_SUFFIX = ".package-info"; 085 086 private static final TypeFilter[] DEFAULT_ENTITY_TYPE_FILTERS = new TypeFilter[] { 087 new AnnotationTypeFilter(Entity.class, false), 088 new AnnotationTypeFilter(Embeddable.class, false), 089 new AnnotationTypeFilter(MappedSuperclass.class, false)}; 090 091 private static final TypeFilter CONVERTER_TYPE_FILTER = new AnnotationTypeFilter(Converter.class, false); 092 093 094 private final ResourcePatternResolver resourcePatternResolver; 095 096 private TypeFilter[] entityTypeFilters = DEFAULT_ENTITY_TYPE_FILTERS; 097 098 099 /** 100 * Create a new LocalSessionFactoryBuilder for the given DataSource. 101 * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using 102 * (may be {@code null}) 103 */ 104 public LocalSessionFactoryBuilder(DataSource dataSource) { 105 this(dataSource, new PathMatchingResourcePatternResolver()); 106 } 107 108 /** 109 * Create a new LocalSessionFactoryBuilder for the given DataSource. 110 * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using 111 * (may be {@code null}) 112 * @param classLoader the ClassLoader to load application classes from 113 */ 114 public LocalSessionFactoryBuilder(DataSource dataSource, ClassLoader classLoader) { 115 this(dataSource, new PathMatchingResourcePatternResolver(classLoader)); 116 } 117 118 /** 119 * Create a new LocalSessionFactoryBuilder for the given DataSource. 120 * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using 121 * (may be {@code null}) 122 * @param resourceLoader the ResourceLoader to load application classes from 123 */ 124 public LocalSessionFactoryBuilder(DataSource dataSource, ResourceLoader resourceLoader) { 125 this(dataSource, resourceLoader, new MetadataSources( 126 new BootstrapServiceRegistryBuilder().applyClassLoader(resourceLoader.getClassLoader()).build())); 127 } 128 129 /** 130 * Create a new LocalSessionFactoryBuilder for the given DataSource. 131 * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using 132 * (may be {@code null}) 133 * @param resourceLoader the ResourceLoader to load application classes from 134 * @param metadataSources the Hibernate MetadataSources service to use (e.g. reusing an existing one) 135 * @since 4.3 136 */ 137 public LocalSessionFactoryBuilder(DataSource dataSource, ResourceLoader resourceLoader, MetadataSources metadataSources) { 138 super(metadataSources); 139 140 getProperties().put(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName()); 141 if (dataSource != null) { 142 getProperties().put(AvailableSettings.DATASOURCE, dataSource); 143 } 144 145 // Hibernate 5.1/5.2: manually enforce connection release mode ON_CLOSE (the former default) 146 try { 147 // Try Hibernate 5.2 148 AvailableSettings.class.getField("CONNECTION_HANDLING"); 149 getProperties().put("hibernate.connection.handling_mode", "DELAYED_ACQUISITION_AND_HOLD"); 150 } 151 catch (NoSuchFieldException ex) { 152 // Try Hibernate 5.1 153 try { 154 AvailableSettings.class.getField("ACQUIRE_CONNECTIONS"); 155 getProperties().put("hibernate.connection.release_mode", "ON_CLOSE"); 156 } 157 catch (NoSuchFieldException ex2) { 158 // on Hibernate 5.0.x or lower - no need to change the default there 159 } 160 } 161 162 getProperties().put(AvailableSettings.CLASSLOADERS, Collections.singleton(resourceLoader.getClassLoader())); 163 this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); 164 } 165 166 167 /** 168 * Set the Spring {@link JtaTransactionManager} or the JTA {@link TransactionManager} 169 * to be used with Hibernate, if any. Allows for using a Spring-managed transaction 170 * manager for Hibernate 5's session and cache synchronization, with the 171 * "hibernate.transaction.jta.platform" automatically set to it. 172 * <p>A passed-in Spring {@link JtaTransactionManager} needs to contain a JTA 173 * {@link TransactionManager} reference to be usable here, except for the WebSphere 174 * case where we'll automatically set {@code WebSphereExtendedJtaPlatform} accordingly. 175 * <p>Note: If this is set, the Hibernate settings should not contain a JTA platform 176 * setting to avoid meaningless double configuration. 177 */ 178 public LocalSessionFactoryBuilder setJtaTransactionManager(Object jtaTransactionManager) { 179 Assert.notNull(jtaTransactionManager, "Transaction manager reference must not be null"); 180 181 if (jtaTransactionManager instanceof JtaTransactionManager) { 182 boolean webspherePresent = ClassUtils.isPresent("com.ibm.wsspi.uow.UOWManager", getClass().getClassLoader()); 183 if (webspherePresent) { 184 getProperties().put(AvailableSettings.JTA_PLATFORM, 185 "org.hibernate.engine.transaction.jta.platform.internal.WebSphereExtendedJtaPlatform"); 186 } 187 else { 188 JtaTransactionManager jtaTm = (JtaTransactionManager) jtaTransactionManager; 189 if (jtaTm.getTransactionManager() == null) { 190 throw new IllegalArgumentException( 191 "Can only apply JtaTransactionManager which has a TransactionManager reference set"); 192 } 193 getProperties().put(AvailableSettings.JTA_PLATFORM, 194 new ConfigurableJtaPlatform(jtaTm.getTransactionManager(), jtaTm.getUserTransaction(), 195 jtaTm.getTransactionSynchronizationRegistry())); 196 } 197 } 198 else if (jtaTransactionManager instanceof TransactionManager) { 199 getProperties().put(AvailableSettings.JTA_PLATFORM, 200 new ConfigurableJtaPlatform((TransactionManager) jtaTransactionManager, null, null)); 201 } 202 else { 203 throw new IllegalArgumentException( 204 "Unknown transaction manager type: " + jtaTransactionManager.getClass().getName()); 205 } 206 207 // Hibernate 5.1/5.2: manually enforce connection release mode AFTER_STATEMENT (the JTA default) 208 try { 209 // Try Hibernate 5.2 210 AvailableSettings.class.getField("CONNECTION_HANDLING"); 211 getProperties().put("hibernate.connection.handling_mode", "DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT"); 212 } 213 catch (NoSuchFieldException ex) { 214 // Try Hibernate 5.1 215 try { 216 AvailableSettings.class.getField("ACQUIRE_CONNECTIONS"); 217 getProperties().put("hibernate.connection.release_mode", "AFTER_STATEMENT"); 218 } 219 catch (NoSuchFieldException ex2) { 220 // on Hibernate 5.0.x or lower - no need to change the default there 221 } 222 } 223 224 return this; 225 } 226 227 /** 228 * Set a {@link MultiTenantConnectionProvider} to be passed on to the SessionFactory. 229 * @since 4.3 230 * @see AvailableSettings#MULTI_TENANT_CONNECTION_PROVIDER 231 */ 232 public LocalSessionFactoryBuilder setMultiTenantConnectionProvider(MultiTenantConnectionProvider multiTenantConnectionProvider) { 233 getProperties().put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); 234 return this; 235 } 236 237 /** 238 * Overridden to reliably pass a {@link CurrentTenantIdentifierResolver} to the SessionFactory. 239 * @since 4.3.2 240 * @see AvailableSettings#MULTI_TENANT_IDENTIFIER_RESOLVER 241 */ 242 @Override 243 public void setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { 244 getProperties().put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); 245 super.setCurrentTenantIdentifierResolver(currentTenantIdentifierResolver); 246 } 247 248 /** 249 * Specify custom type filters for Spring-based scanning for entity classes. 250 * <p>Default is to search all specified packages for classes annotated with 251 * {@code @javax.persistence.Entity}, {@code @javax.persistence.Embeddable} 252 * or {@code @javax.persistence.MappedSuperclass}. 253 * @see #scanPackages 254 */ 255 public LocalSessionFactoryBuilder setEntityTypeFilters(TypeFilter... entityTypeFilters) { 256 this.entityTypeFilters = entityTypeFilters; 257 return this; 258 } 259 260 /** 261 * Add the given annotated classes in a batch. 262 * @see #addAnnotatedClass 263 * @see #scanPackages 264 */ 265 public LocalSessionFactoryBuilder addAnnotatedClasses(Class<?>... annotatedClasses) { 266 for (Class<?> annotatedClass : annotatedClasses) { 267 addAnnotatedClass(annotatedClass); 268 } 269 return this; 270 } 271 272 /** 273 * Add the given annotated packages in a batch. 274 * @see #addPackage 275 * @see #scanPackages 276 */ 277 public LocalSessionFactoryBuilder addPackages(String... annotatedPackages) { 278 for (String annotatedPackage : annotatedPackages) { 279 addPackage(annotatedPackage); 280 } 281 return this; 282 } 283 284 /** 285 * Perform Spring-based scanning for entity classes, registering them 286 * as annotated classes with this {@code Configuration}. 287 * @param packagesToScan one or more Java package names 288 * @throws HibernateException if scanning fails for any reason 289 */ 290 @SuppressWarnings("unchecked") 291 public LocalSessionFactoryBuilder scanPackages(String... packagesToScan) throws HibernateException { 292 Set<String> entityClassNames = new TreeSet<String>(); 293 Set<String> converterClassNames = new TreeSet<String>(); 294 Set<String> packageNames = new TreeSet<String>(); 295 try { 296 for (String pkg : packagesToScan) { 297 String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + 298 ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN; 299 Resource[] resources = this.resourcePatternResolver.getResources(pattern); 300 MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); 301 for (Resource resource : resources) { 302 if (resource.isReadable()) { 303 MetadataReader reader = readerFactory.getMetadataReader(resource); 304 String className = reader.getClassMetadata().getClassName(); 305 if (matchesEntityTypeFilter(reader, readerFactory)) { 306 entityClassNames.add(className); 307 } 308 else if (CONVERTER_TYPE_FILTER.match(reader, readerFactory)) { 309 converterClassNames.add(className); 310 } 311 else if (className.endsWith(PACKAGE_INFO_SUFFIX)) { 312 packageNames.add(className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length())); 313 } 314 } 315 } 316 } 317 } 318 catch (IOException ex) { 319 throw new MappingException("Failed to scan classpath for unlisted classes", ex); 320 } 321 try { 322 ClassLoader cl = this.resourcePatternResolver.getClassLoader(); 323 for (String className : entityClassNames) { 324 addAnnotatedClass(cl.loadClass(className)); 325 } 326 for (String className : converterClassNames) { 327 addAttributeConverter((Class<? extends AttributeConverter<?, ?>>) cl.loadClass(className)); 328 } 329 for (String packageName : packageNames) { 330 addPackage(packageName); 331 } 332 } 333 catch (ClassNotFoundException ex) { 334 throw new MappingException("Failed to load annotated classes from classpath", ex); 335 } 336 return this; 337 } 338 339 /** 340 * Check whether any of the configured entity type filters matches 341 * the current class descriptor contained in the metadata reader. 342 */ 343 private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException { 344 if (this.entityTypeFilters != null) { 345 for (TypeFilter filter : this.entityTypeFilters) { 346 if (filter.match(reader, readerFactory)) { 347 return true; 348 } 349 } 350 } 351 return false; 352 } 353 354 /** 355 * Build the Hibernate {@code SessionFactory} through background bootstrapping, 356 * using the given executor for a parallel initialization phase 357 * (e.g. a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}). 358 * <p>{@code SessionFactory} initialization will then switch into background 359 * bootstrap mode, with a {@code SessionFactory} proxy immediately returned for 360 * injection purposes instead of waiting for Hibernate's bootstrapping to complete. 361 * However, note that the first actual call to a {@code SessionFactory} method will 362 * then block until Hibernate's bootstrapping completed, if not ready by then. 363 * For maximum benefit, make sure to avoid early {@code SessionFactory} calls 364 * in init methods of related beans, even for metadata introspection purposes. 365 * @since 4.3 366 * @see #buildSessionFactory() 367 */ 368 public SessionFactory buildSessionFactory(AsyncTaskExecutor bootstrapExecutor) { 369 Assert.notNull(bootstrapExecutor, "AsyncTaskExecutor must not be null"); 370 return (SessionFactory) Proxy.newProxyInstance(this.resourcePatternResolver.getClassLoader(), 371 new Class<?>[] {SessionFactoryImplementor.class, InfrastructureProxy.class}, 372 new BootstrapSessionFactoryInvocationHandler(bootstrapExecutor)); 373 } 374 375 376 /** 377 * Proxy invocation handler for background bootstrapping, only enforcing 378 * a fully initialized target {@code SessionFactory} when actually needed. 379 * @since 4.3 380 */ 381 private class BootstrapSessionFactoryInvocationHandler implements InvocationHandler { 382 383 private final Future<SessionFactory> sessionFactoryFuture; 384 385 public BootstrapSessionFactoryInvocationHandler(AsyncTaskExecutor bootstrapExecutor) { 386 this.sessionFactoryFuture = bootstrapExecutor.submit(new Callable<SessionFactory>() { 387 @Override 388 public SessionFactory call() throws Exception { 389 return buildSessionFactory(); 390 } 391 }); 392 } 393 394 @Override 395 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 396 try { 397 if (method.getName().equals("equals")) { 398 // Only consider equal when proxies are identical. 399 return (proxy == args[0]); 400 } 401 else if (method.getName().equals("hashCode")) { 402 // Use hashCode of EntityManagerFactory proxy. 403 return System.identityHashCode(proxy); 404 } 405 else if (method.getName().equals("getProperties")) { 406 return getProperties(); 407 } 408 else if (method.getName().equals("getWrappedObject")) { 409 // Call coming in through InfrastructureProxy interface... 410 return getSessionFactory(); 411 } 412 // Regular delegation to the target SessionFactory, 413 // enforcing its full initialization... 414 return method.invoke(getSessionFactory(), args); 415 } 416 catch (InvocationTargetException ex) { 417 throw ex.getTargetException(); 418 } 419 } 420 421 private SessionFactory getSessionFactory() { 422 try { 423 return this.sessionFactoryFuture.get(); 424 } 425 catch (InterruptedException ex) { 426 Thread.currentThread().interrupt(); 427 throw new IllegalStateException("Interrupted during initialization of Hibernate SessionFactory", ex); 428 } 429 catch (ExecutionException ex) { 430 Throwable cause = ex.getCause(); 431 if (cause instanceof HibernateException) { 432 // Rethrow a provider configuration exception (possibly with a nested cause) directly 433 throw (HibernateException) cause; 434 } 435 throw new IllegalStateException("Failed to asynchronously initialize Hibernate SessionFactory: " + 436 ex.getMessage(), cause); 437 } 438 } 439 } 440 441}