jdaugherty commented on code in PR #15568: URL: https://github.com/apache/grails-core/pull/15568#discussion_r3196289274
########## grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java: ########## @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.grails.orm.hibernate.cfg; + +import java.io.IOException; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.sql.DataSource; + +import jakarta.annotation.Nullable; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.MappedSuperclass; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.spi.AdditionalMappingContributor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.BytecodeSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.cfg.JdbcSettings; +import org.hibernate.context.spi.CurrentSessionContext; +import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.service.ServiceRegistry; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +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.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.ClassUtils; + +import org.grails.datastore.gorm.GormEntity; +import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings; +import org.grails.datastore.mapping.core.connections.ConnectionSource; +import org.grails.datastore.mapping.model.PersistentEntity; +import org.grails.orm.hibernate.EventListenerIntegrator; +import org.grails.orm.hibernate.GrailsSessionContext; +import org.grails.orm.hibernate.HibernateEventListeners; +import org.grails.orm.hibernate.MetadataIntegrator; +import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder; +import org.grails.orm.hibernate.cfg.domainbinding.util.NamingStrategyProvider; +import org.grails.orm.hibernate.proxy.GrailsBytecodeProvider; + +/** + * A Configuration that uses a MappingContext to configure Hibernate + * + * @since 5.0 + */ +@SuppressWarnings({"rawtypes", "PMD.UseProperClassLoader", "PMD.DataflowAnomalyAnalysis", "PMD.CloseResource"}) +public class HibernateMappingContextConfiguration extends Configuration + implements ApplicationContextAware, Serializable { + + @Serial + private static final long serialVersionUID = -7115087342689305517L; + + private static final String RESOURCE_PATTERN = "/**/*.class"; + + private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] { + new AnnotationTypeFilter(Entity.class, false), + new AnnotationTypeFilter(Embeddable.class, false), + new AnnotationTypeFilter(MappedSuperclass.class, false) + }; + private static final String FALSE_LITERAL = "false"; + private final Class<? extends CurrentSessionContext> currentSessionContext = GrailsSessionContext.class; + // private MetadataContributor metadataContributor; + private final Set<Class> additionalClasses = new HashSet<>(); + protected String sessionFactoryBeanName = "sessionFactory"; + protected String dataSourceName = ConnectionSource.DEFAULT; + protected transient HibernateMappingContext hibernateMappingContext; + private transient HibernateEventListeners hibernateEventListeners; + private Map<String, Object> eventListeners; + private transient ServiceRegistry serviceRegistry; + private transient ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + private transient NamingStrategyProvider namingStrategyProvider = new NamingStrategyProvider(); + protected GrailsBytecodeProvider bytecodeProvider; + + public void setBytecodeProvider(GrailsBytecodeProvider bytecodeProvider) { + this.bytecodeProvider = bytecodeProvider; + } + + public NamingStrategyProvider getNamingStrategyProvider() { + return namingStrategyProvider; + } + + public void setNamingStrategyProvider(NamingStrategyProvider namingStrategyProvider) { + this.namingStrategyProvider = namingStrategyProvider; + } + + public MappingCacheHolder getMappingCacheHolder() { + return hibernateMappingContext != null ? hibernateMappingContext.getMappingCacheHolder() : null; + } + + public void setHibernateMappingContext(HibernateMappingContext hibernateMappingContext) { + this.hibernateMappingContext = hibernateMappingContext; + } + + @Override + public void setApplicationContext(@Nullable ApplicationContext applicationContext) throws BeansException { + resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(applicationContext); + String dsName = ConnectionSource.DEFAULT.equals(dataSourceName) ? "dataSource" : "dataSource_" + dataSourceName; + Properties properties = getProperties(); + + if (applicationContext != null) { + if (!properties.containsKey(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE) && applicationContext.containsBean(dsName)) { + properties.put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, applicationContext.getBean(dsName)); + } + properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, currentSessionContext.getName()); + properties.put( + "hibernate.enhancer.bytecodeprovider.instance", + getGrailsBytecodeProvider()); + properties.put("hibernate.bytecode.allow_enhancement_as_proxy", FALSE_LITERAL); + properties.put("hibernate.bytecode.enhancement_metadata_cache", FALSE_LITERAL); + properties.put("hibernate.enhancer.enableLazyInitialization", FALSE_LITERAL); + properties.put("hibernate.enhancer.enableDirtyTracking", FALSE_LITERAL); + properties.put("hibernate.enhancer.enableAssociationManagement", FALSE_LITERAL); + ClassLoader classLoader = applicationContext.getClassLoader(); + if (classLoader != null) { + properties.put(AvailableSettings.CLASSLOADERS, classLoader); + } + } + } + + protected GrailsBytecodeProvider getGrailsBytecodeProvider() { + return bytecodeProvider != null ? bytecodeProvider : new GrailsBytecodeProvider(); + } + + /** + * Set the target SQL {@link DataSource} + * + * @param connectionSource The data source to use + */ + public void setDataSourceConnectionSource(ConnectionSource<DataSource, DataSourceSettings> connectionSource) { + this.dataSourceName = connectionSource.getName(); + DataSource source = connectionSource.getSource(); + getProperties().put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, source); + getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, GrailsSessionContext.class.getName()); + setBytecodeProvider(getGrailsBytecodeProvider()); + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (contextClassLoader != null && + contextClassLoader.getClass().getSimpleName().equalsIgnoreCase("RestartClassLoader")) { + getProperties().put(AvailableSettings.CLASSLOADERS, contextClassLoader); + } else { + getProperties() + .put( + AvailableSettings.CLASSLOADERS, + connectionSource.getClass().getClassLoader()); + } + } + + /** + * Add the given annotated classes in a batch. + * + * @return Configuration + * @see #addAnnotatedClass + * @see #scanPackages + */ + @Override + public Configuration addAnnotatedClasses(Class... annotatedClasses) { + for (Class<?> annotatedClass : annotatedClasses) { + addAnnotatedClass(annotatedClass); + } + return this; + } + + @Override + public Configuration addAnnotatedClass(Class annotatedClass) { + additionalClasses.add(annotatedClass); + return super.addAnnotatedClass(annotatedClass); + } + + @Override + public HibernateMappingContextConfiguration addPackages(String... annotatedPackages) { + for (String annotatedPackage : annotatedPackages) { + addPackage(annotatedPackage); + } + return this; + } + + /** + * Perform Spring-based scanning for entity classes, registering them as annotated classes with + * this {@code Configuration}. + * + * @param packagesToScan one or more Java package names + * @throws HibernateException if scanning fails for any reason + */ + public void scanPackages(String... packagesToScan) throws HibernateException { + try { + MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver); + for (String pkg : packagesToScan) { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(pkg) + + RESOURCE_PATTERN; + Resource[] resources = resourcePatternResolver.getResources(pattern); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader reader = readerFactory.getMetadataReader(resource); + String className = reader.getClassMetadata().getClassName(); + if (matchesFilter(reader, readerFactory)) { + ClassLoader classLoader = resourcePatternResolver.getClassLoader(); + Class<?> loadedClass = classLoader != null ? + classLoader.loadClass(className) : + ClassUtils.forName(className, null); + addAnnotatedClasses(loadedClass); + } + } + } + } + } catch (IOException ex) { + throw new MappingException("Failed to scan classpath for unlisted classes", ex); + } catch (ClassNotFoundException ex) { + throw new MappingException("Failed to load annotated classes from classpath", ex); + } + } + + /** + * Check whether any of the configured entity type filters matches the current class descriptor + * contained in the metadata reader. + */ + protected boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException { + for (TypeFilter filter : ENTITY_TYPE_FILTERS) { + if (filter.match(reader, readerFactory)) { + return true; + } + } + return false; + } + + public void setSessionFactoryBeanName(String name) { + sessionFactoryBeanName = name; + } + + public void setDataSourceName(String name) { + dataSourceName = name; + } + + /* (non-Javadoc) + * @see org.hibernate.cfg.Configuration#buildSessionFactory() + */ + @Override + public SessionFactory buildSessionFactory() throws HibernateException { + // 1. FORCE the custom bytecode provider instance right before bootstrap + // This bypasses the ServiceLoader and ensures your GrailsBytecodeProvider is used. + GrailsBytecodeProvider bytecodeProvider = getGrailsBytecodeProvider(); + getProperties() + .put( + BytecodeSettings.BYTECODE_PROVIDER_INSTANCE, + bytecodeProvider); + + // set the class loader to load Groovy classes + + // work around for HHH-2624 + SessionFactory sessionFactory; + + Object classLoaderObject = getProperties().get(AvailableSettings.CLASSLOADERS); + ClassLoader appClassLoader; + + if (classLoaderObject instanceof ClassLoader) { + appClassLoader = (ClassLoader) classLoaderObject; + } else { + appClassLoader = getClass().getClassLoader(); + } + + ConfigurationHelper.resolvePlaceHolders(getProperties()); + + final GrailsDomainBinder domainBinder = new GrailsDomainBinder( + dataSourceName, + sessionFactoryBeanName, + hibernateMappingContext, + namingStrategyProvider, + hibernateMappingContext.getMappingCacheHolder()); + + List<Class> annotatedClasses = new ArrayList<>(); + for (PersistentEntity persistentEntity : hibernateMappingContext.getPersistentEntities()) { + Class<?> javaClass = persistentEntity.getJavaClass(); + if (javaClass.isAnnotationPresent(Entity.class)) { + annotatedClasses.add(javaClass); + } + } + + if (!additionalClasses.isEmpty()) { + for (Class additionalClass : additionalClasses) { + if (GormEntity.class.isAssignableFrom(additionalClass)) { + hibernateMappingContext.addPersistentEntity(additionalClass); + } + } + } + + addAnnotatedClasses(annotatedClasses.toArray(new Class[0])); + + ClassLoaderService classLoaderService = new ClassLoaderServiceImpl(appClassLoader) { + @Override + public <S> Collection<S> loadJavaServices(Class<S> serviceContract) { + // Ensure Grails contributes mappings for GORM entities even if they lack JPA @Entity + if (AdditionalMappingContributor.class.isAssignableFrom(serviceContract)) { + // Include the GrailsDomainBinder first, then any other contributors + // discovered by the parent classloader (e.g., Envers AdditionalMappingContributorImpl). + // Without this, Envers' AdditionalMappingContributor would be excluded, + // preventing EnversService from being initialized before EnversIntegrator runs. + Collection<S> parentContributors = super.loadJavaServices(serviceContract); + @SuppressWarnings("unchecked") + S grailsBinder = (S) domainBinder; + List<S> allContributors = new ArrayList<>(parentContributors.size() + 1); + allContributors.add(grailsBinder); + allContributors.addAll(parentContributors); + return allContributors; Review Comment: Good fix. Previously `Collections.singletonList(domainBinder)` silently excluded all other `AdditionalMappingContributor` implementations (including Envers' `AdditionalMappingContributorImpl`), causing `EnversService` to be uninitialized before `EnversIntegrator` ran. Prepending the Grails binder before the parent contributors is the correct ordering. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
