diff --git a/src/main/java/org/hibernate/boot/models/source/internal/ModelProcessingContextImpl.java b/src/main/java/org/hibernate/boot/models/source/internal/ModelProcessingContextImpl.java index bae36c7..4c4cac7 100644 --- a/src/main/java/org/hibernate/boot/models/source/internal/ModelProcessingContextImpl.java +++ b/src/main/java/org/hibernate/boot/models/source/internal/ModelProcessingContextImpl.java @@ -39,6 +39,7 @@ import org.hibernate.boot.models.source.spi.AnnotationUsage; import org.hibernate.boot.models.source.spi.ClassDetailsRegistry; import org.hibernate.boot.models.spi.ModelProcessingContext; +import org.hibernate.boot.spi.ClassLoaderAccess; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.internal.util.collections.CollectionHelper; @@ -50,27 +51,22 @@ * @author Steve Ebersole */ public class ModelProcessingContextImpl implements ModelProcessingContext { + private final MetadataBuildingContext buildingContext; + private final ClassLoaderAccess classLoaderAccess; private final AnnotationDescriptorRegistryImpl descriptorRegistry; private final ClassDetailsRegistry classDetailsRegistry; - private final MetadataBuildingContext buildingContext; private final Map,List>> annotationUsageMap = new HashMap<>(); public ModelProcessingContextImpl(MetadataBuildingContext buildingContext) { - this.buildingContext = buildingContext; - this.descriptorRegistry = new AnnotationDescriptorRegistryImpl( this ); - this.classDetailsRegistry = new ClassDetailsRegistryImpl( this ); - - primeRegistries(); + this( buildingContext, buildingContext.getBootstrapContext().getClassLoaderAccess() ); } - public ModelProcessingContextImpl( - ClassDetailsRegistry classDetailsRegistry, - AnnotationDescriptorRegistryImpl annotationDescriptorRegistry, - MetadataBuildingContext buildingContext) { + public ModelProcessingContextImpl(MetadataBuildingContext buildingContext, ClassLoaderAccess classLoaderAccess) { this.buildingContext = buildingContext; - this.descriptorRegistry = annotationDescriptorRegistry; - this.classDetailsRegistry = classDetailsRegistry; + this.classLoaderAccess = classLoaderAccess; + this.descriptorRegistry = new AnnotationDescriptorRegistryImpl( this ); + this.classDetailsRegistry = new ClassDetailsRegistryImpl( this ); primeRegistries(); } @@ -142,6 +138,11 @@ public ClassDetailsRegistry getClassDetailsRegistry() { return classDetailsRegistry; } + @Override + public ClassLoaderAccess getClassLoaderAccess() { + return classLoaderAccess; + } + @Override public MetadataBuildingContext getMetadataBuildingContext() { return buildingContext; diff --git a/src/main/java/org/hibernate/boot/models/source/internal/hcann/ClassDetailsBuilderImpl.java b/src/main/java/org/hibernate/boot/models/source/internal/hcann/ClassDetailsBuilderImpl.java index d031ff0..ca3e724 100644 --- a/src/main/java/org/hibernate/boot/models/source/internal/hcann/ClassDetailsBuilderImpl.java +++ b/src/main/java/org/hibernate/boot/models/source/internal/hcann/ClassDetailsBuilderImpl.java @@ -11,7 +11,7 @@ import org.hibernate.boot.models.source.spi.ClassDetails; import org.hibernate.boot.models.source.spi.ClassDetailsBuilder; import org.hibernate.boot.models.spi.ModelProcessingContext; -import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.spi.ClassLoaderAccess; /** * HCANN based ClassDetailsBuilder @@ -19,14 +19,11 @@ * @author Steve Ebersole */ public class ClassDetailsBuilderImpl implements ClassDetailsBuilder { - private final ClassLoaderService classLoaderService; + private final ClassLoaderAccess classLoaderAccess; private final ReflectionManager hcannReflectionManager; public ClassDetailsBuilderImpl(ModelProcessingContext processingContext) { - this.classLoaderService = processingContext.getMetadataBuildingContext() - .getBootstrapContext() - .getServiceRegistry() - .getService( ClassLoaderService.class ); + this.classLoaderAccess = processingContext.getClassLoaderAccess(); this.hcannReflectionManager = processingContext.getMetadataBuildingContext() .getBootstrapContext() .getReflectionManager(); @@ -34,7 +31,7 @@ public ClassDetailsBuilderImpl(ModelProcessingContext processingContext) { @Override public ClassDetails buildClassDetails(String name, ModelProcessingContext processingContext) { - final Class classForName = classLoaderService.classForName( name ); + final Class classForName = classLoaderAccess.classForName( name ); final XClass xClassForName = hcannReflectionManager.toXClass( classForName ); return new ClassDetailsImpl( xClassForName, processingContext ); } diff --git a/src/main/java/org/hibernate/boot/models/source/internal/reflection/ClassDetailsBuilderImpl.java b/src/main/java/org/hibernate/boot/models/source/internal/reflection/ClassDetailsBuilderImpl.java index d169f80..cfee446 100644 --- a/src/main/java/org/hibernate/boot/models/source/internal/reflection/ClassDetailsBuilderImpl.java +++ b/src/main/java/org/hibernate/boot/models/source/internal/reflection/ClassDetailsBuilderImpl.java @@ -31,8 +31,7 @@ public static ClassDetails buildClassDetailsStatic(String name, ModelProcessingC return buildClassDetails( processingContext.getMetadataBuildingContext() .getBootstrapContext() - .getServiceRegistry() - .getService( ClassLoaderService.class ) + .getClassLoaderAccess() .classForName( name ), processingContext ); diff --git a/src/main/java/org/hibernate/boot/models/spi/ModelProcessingContext.java b/src/main/java/org/hibernate/boot/models/spi/ModelProcessingContext.java index 5243de5..8a81409 100644 --- a/src/main/java/org/hibernate/boot/models/spi/ModelProcessingContext.java +++ b/src/main/java/org/hibernate/boot/models/spi/ModelProcessingContext.java @@ -14,6 +14,7 @@ import org.hibernate.boot.models.source.spi.AnnotationDescriptorRegistry; import org.hibernate.boot.models.source.spi.AnnotationUsage; import org.hibernate.boot.models.source.spi.ClassDetailsRegistry; +import org.hibernate.boot.spi.ClassLoaderAccess; import org.hibernate.boot.spi.MetadataBuildingContext; /** @@ -38,6 +39,8 @@ public interface ModelProcessingContext { */ ClassDetailsRegistry getClassDetailsRegistry(); + ClassLoaderAccess getClassLoaderAccess(); + void registerUsage(AnnotationUsage usage); List> getAllUsages(AnnotationDescriptor annotationDescriptor); diff --git a/src/test/java/org/hibernate/orm/test/boot/models/ModelHelper.java b/src/test/java/org/hibernate/orm/test/boot/models/ModelHelper.java index f6c1b12..42e4d26 100644 --- a/src/test/java/org/hibernate/orm/test/boot/models/ModelHelper.java +++ b/src/test/java/org/hibernate/orm/test/boot/models/ModelHelper.java @@ -10,13 +10,16 @@ import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.java.JavaReflectionManager; +import org.hibernate.boot.internal.ClassLoaderAccessImpl; import org.hibernate.boot.models.intermediate.internal.EntityHierarchyBuilder; import org.hibernate.boot.models.intermediate.spi.EntityHierarchy; import org.hibernate.boot.models.source.internal.ModelProcessingContextImpl; import org.hibernate.boot.models.source.internal.hcann.ClassDetailsImpl; import org.hibernate.boot.models.spi.ModelProcessingContext; import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.spi.ClassLoaderAccess; +import org.hibernate.testing.boot.ClassLoaderAccessTestingImpl; import org.hibernate.testing.boot.MetadataBuildingContextTestingImpl; /** @@ -28,6 +31,11 @@ public static ModelProcessingContext buildProcessingContext(StandardServiceRegis return new ModelProcessingContextImpl( buildingContext ); } + public static ModelProcessingContext buildProcessingContext(StandardServiceRegistry registry, ClassLoaderAccess classLoaderAccess) { + final MetadataBuildingContextTestingImpl buildingContext = new MetadataBuildingContextTestingImpl( registry ); + return new ModelProcessingContextImpl( buildingContext, classLoaderAccess ); + } + public static Set buildHierarchies(StandardServiceRegistry registry, Class... classes) { return buildHierarchies( buildProcessingContext( registry ), classes ); } diff --git a/src/test/java/org/hibernate/orm/test/boot/models/source/ClassLoaderAccessTests.java b/src/test/java/org/hibernate/orm/test/boot/models/source/ClassLoaderAccessTests.java new file mode 100644 index 0000000..48a29ed --- /dev/null +++ b/src/test/java/org/hibernate/orm/test/boot/models/source/ClassLoaderAccessTests.java @@ -0,0 +1,78 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +package org.hibernate.orm.test.boot.models.source; + +import org.hibernate.boot.models.source.internal.hcann.ClassDetailsImpl; +import org.hibernate.boot.models.source.spi.ClassDetails; +import org.hibernate.boot.models.source.spi.ClassDetailsRegistry; +import org.hibernate.boot.models.spi.ModelProcessingContext; +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.orm.test.boot.models.ModelHelper; +import org.hibernate.orm.test.util.ClassLoaderAccessImpl; +import org.hibernate.orm.test.util.CollectingClassLoaderService; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Assert expectations about {@link Class} references and how they got loaded + * + * @author Steve Ebersole + */ +public class ClassLoaderAccessTests { + public static final String ENTITY_CLASS_NAME = "org.hibernate.orm.test.boot.models.source.TestEntity"; + + @Test + public void testClassLoaderIsolation() { + final CollectingClassLoaderService classLoaderService = new CollectingClassLoaderService(); + final BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder() + .applyClassLoaderService( classLoaderService ) + .build(); + final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder( bootstrapRegistry ).build(); + + final ModelProcessingContext processingContext = ModelHelper.buildProcessingContext( serviceRegistry ); + final ClassDetailsRegistry classDetailsRegistry = processingContext.getClassDetailsRegistry(); + + final ClassDetails classDetails = classDetailsRegistry.resolveClassDetails( ENTITY_CLASS_NAME ); + // assert it was handled via the HCANN builder + assertThat( classDetails ).isInstanceOf( ClassDetailsImpl.class ); + + // Currently ORM completely ignores the temp classloader. That manifests here as the entity class being loaded on the "live" class-loader + assertThat( classLoaderService.getCachedClassForName( ENTITY_CLASS_NAME ) ).isNotNull(); + } + + @Test + public void testClassLoaderIsolation2() { + // Unlike `#testClassLoaderIsolation`, here we use a ClassLoaderAccess which incorporates the temp ClassLoader. + + final CollectingClassLoaderService classLoaderService = new CollectingClassLoaderService(); + final BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder() + .applyClassLoaderService( classLoaderService ) + .build(); + final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder( bootstrapRegistry ).build(); + + final ClassLoader tmpClassLoader = new ClassLoader() {}; + final ClassLoaderAccessImpl classLoaderAccess = new ClassLoaderAccessImpl( tmpClassLoader, classLoaderService ); + + final ModelProcessingContext processingContext = ModelHelper.buildProcessingContext( serviceRegistry, classLoaderAccess ); + final ClassDetailsRegistry classDetailsRegistry = processingContext.getClassDetailsRegistry(); + + final ClassDetails classDetails = classDetailsRegistry.resolveClassDetails( ENTITY_CLASS_NAME ); + // assert it was handled via the HCANN builder + assertThat( classDetails ).isInstanceOf( ClassDetailsImpl.class ); + + // Now, the entity is not loaded into the "live" class-loader + assertThat( classLoaderService.getCachedClassForName( ENTITY_CLASS_NAME ) ).isNull(); + assertThat( classLoaderAccess.getClassesLoadedFromTempClassLoader() ).containsOnly( ENTITY_CLASS_NAME ); + } + +} diff --git a/src/test/java/org/hibernate/orm/test/boot/models/source/TestEntity.java b/src/test/java/org/hibernate/orm/test/boot/models/source/TestEntity.java new file mode 100644 index 0000000..4a1875f --- /dev/null +++ b/src/test/java/org/hibernate/orm/test/boot/models/source/TestEntity.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +package org.hibernate.orm.test.boot.models.source; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Steve Ebersole + */ +@Entity +public class TestEntity { + @Id + private Integer id; + @Basic + private String name; + + private TestEntity() { + // for use by Hibernate + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/org/hibernate/orm/test/util/ClassLoaderAccessImpl.java b/src/test/java/org/hibernate/orm/test/util/ClassLoaderAccessImpl.java new file mode 100644 index 0000000..19feff9 --- /dev/null +++ b/src/test/java/org/hibernate/orm/test/util/ClassLoaderAccessImpl.java @@ -0,0 +1,100 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.util; + +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.boot.spi.ClassLoaderAccess; + +import org.jboss.logging.Logger; + +/** + * Mostly a copy of {@link org.hibernate.boot.internal.ClassLoaderAccessImpl}, just to + * "override" {@link #isSafeClass(String)} + * + * @author Steve Ebersole + */ +public class ClassLoaderAccessImpl implements ClassLoaderAccess { + private static final Logger log = Logger.getLogger( ClassLoaderAccessImpl.class ); + + private final ClassLoaderService classLoaderService; + private final ClassLoader tempClassLoader; + + private final Set classesLoadedFromTempClassLoader; + + public ClassLoaderAccessImpl(ClassLoader tempClassLoader, ClassLoaderService classLoaderService) { + this.tempClassLoader = tempClassLoader; + this.classLoaderService = classLoaderService; + + this.classesLoadedFromTempClassLoader = tempClassLoader == null + ? Collections.emptySet() + : new HashSet<>(); + } + + @Override + @SuppressWarnings("unchecked") + public Class classForName(String name) { + if ( name == null ) { + throw new IllegalArgumentException( "Name of class to load cannot be null" ); + } + + if ( isSafeClass( name ) ) { + return classLoaderService.classForName( name ); + } + else { + log.debugf( "Not known whether passed class name [%s] is safe", name ); + if ( tempClassLoader == null ) { + log.debugf( + "No temp ClassLoader provided; using live ClassLoader " + + "for loading potentially unsafe class : %s", + name + ); + return classLoaderService.classForName( name ); + } + else { + log.debugf( + "Temp ClassLoader was provided, so we will use that : %s", + name + ); + try { + classesLoadedFromTempClassLoader.add( name ); + return tempClassLoader.loadClass( name ); + } + catch (ClassNotFoundException e) { + throw new ClassLoadingException( name ); + } + } + } + } + + private boolean isSafeClass(String name) { + // classes in any of these packages are safe to load through the "live" ClassLoader + return name.startsWith( "java." ) + || name.startsWith( "javax." ) + || name.startsWith( "jakarta." ) + || ( name.startsWith( "org.hibernate." ) && !name.contains( ".test." ) ); + + } + + public ClassLoader getTempClassLoader() { + return tempClassLoader; + } + + public Set getClassesLoadedFromTempClassLoader() { + return classesLoadedFromTempClassLoader; + } + + @Override + public URL locateResource(String resourceName) { + return classLoaderService.locateResource( resourceName ); + } +} diff --git a/src/test/java/org/hibernate/orm/test/util/CollectingClassLoaderService.java b/src/test/java/org/hibernate/orm/test/util/CollectingClassLoaderService.java new file mode 100644 index 0000000..8ffc756 --- /dev/null +++ b/src/test/java/org/hibernate/orm/test/util/CollectingClassLoaderService.java @@ -0,0 +1,101 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +package org.hibernate.orm.test.util; + +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; + +/** + * @author Steve Ebersole + */ +public class CollectingClassLoaderService implements ClassLoaderService { + private final ClassLoaderService realClassLoaderService; + private final Map> classMap; + + public CollectingClassLoaderService() { + this.realClassLoaderService = new ClassLoaderServiceImpl(); + this.classMap = new HashMap<>(); + } + + public Class getCachedClassForName(String className) { + //noinspection unchecked + return (Class) classMap.get( className ); + } + + @Override + public Class classForName(String className) { + final Class existing = classMap.get( className ); + if ( existing != null ) { + if ( existing == Void.class ) { + throw new ClassLoadingException( "Unknown class - `" + className + "`" ); + } + //noinspection unchecked + return (Class) existing; + } + + try { + final Class loaded = realClassLoaderService.classForName( className ); + classMap.put( className, loaded ); + return loaded; + } + catch (ClassLoadingException e) { + classMap.put( className, Void.class ); + throw e; + } + } + + @Override + public URL locateResource(String name) { + return realClassLoaderService.locateResource( name ); + } + + @Override + public InputStream locateResourceStream(String name) { + return realClassLoaderService.locateResourceStream( name ); + } + + @Override + public List locateResources(String name) { + return realClassLoaderService.locateResources( name ); + } + + @Override + public Collection loadJavaServices(Class serviceContract) { + return realClassLoaderService.loadJavaServices( serviceContract ); + } + + @Override + public T generateProxy(InvocationHandler handler, Class... interfaces) { + return realClassLoaderService.generateProxy( handler, interfaces ); + } + + @Override + public Package packageForNameOrNull(String packageName) { + return realClassLoaderService.packageForNameOrNull( packageName ); + } + + @Override + public T workWithClassLoader(Work work) { + return realClassLoaderService.workWithClassLoader( work ); + } + + @Override + public void stop() { + classMap.clear(); + realClassLoaderService.stop(); + } +}