From c166b90bfc028033c532b09f2a21d6c8542c97e6 Mon Sep 17 00:00:00 2001 From: Sanne Grinovero Date: Thu, 25 Apr 2024 16:36:46 +0100 Subject: [PATCH] HHH-18011 Extract DefaultEnhancerClassFileLocator and allow using a different implementation --- .../ByteBuddyEnhancementContext.java | 11 +- .../internal/bytebuddy/CoreTypePool.java | 98 ++++++++++++++++ .../bytebuddy/EnhancerClassLocator.java | 36 ++++++ .../internal/bytebuddy/EnhancerImpl.java | 73 ++++-------- .../internal/bytebuddy/ModelTypePool.java | 108 ++++++++++++++++++ .../bytebuddy/OverridingClassFileLocator.java | 52 +++++++++ .../bytebuddy/BytecodeProviderImpl.java | 14 +++ 7 files changed, 337 insertions(+), 55 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java create mode 100644 hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassLocator.java create mode 100644 hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java create mode 100644 hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java index 0b1f68e60160..5e495b30f1ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java @@ -7,6 +7,7 @@ package org.hibernate.bytecode.enhance.internal.bytebuddy; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -35,10 +36,16 @@ class ByteBuddyEnhancementContext { private final ConcurrentHashMap> getterByTypeMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap locksMap = new ConcurrentHashMap<>(); - ByteBuddyEnhancementContext(EnhancementContext enhancementContext) { - this.enhancementContext = enhancementContext; + ByteBuddyEnhancementContext(final EnhancementContext enhancementContext) { + this.enhancementContext = Objects.requireNonNull( enhancementContext ); } + /** + * @deprecated as it's currently unused and we're not always actually sourcing the classes to be transformed + * from a classloader, so this getter can't always be honoured correctly. + * @return the ClassLoader provided by the underlying EnhancementContext. Might be otherwise ignored. + */ + @Deprecated(forRemoval = true) public ClassLoader getLoadingClassLoader() { return enhancementContext.getLoadingClassLoader(); } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java new file mode 100644 index 000000000000..e4769807bfa4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java @@ -0,0 +1,98 @@ +/* + * 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 . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.concurrent.ConcurrentHashMap; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.pool.TypePool; + +/** + * A TypePool which only loads, and caches, types whose package + * name starts with certain chosen prefixes. + * The default is to only load classes whose package names start with + * either "jakarta." or "java.". + * This allows to reuse these caches independently from application + * code and classloader changes, as during enhancement we frequently + * encounter such symbols as well, for example triggered by JPA annotations + * or properties mapped via standard java types and collections. + * Symbols resolved by this pool are backed by loaded classes from + * ORM's classloader. + */ +public class CoreTypePool extends TypePool.AbstractBase implements TypePool { + + private final ClassLoader hibernateClassLoader = CoreTypePool.class.getClassLoader(); + private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); + private final String[] acceptedPrefixes; + + /** + * Construct a new {@link CoreTypePool} with its default configuration: + * to only load classes whose package names start with either "jakarta." + * or "java." + */ + public CoreTypePool() { + //By default optimise for jakarta annotations, and java util collections + this("jakarta.", "java."); + } + + /** + * Construct a new {@link CoreTypePool} with a choice of which prefixes + * for fully qualified classnames will be loaded by this {@link TypePool}. + */ + public CoreTypePool(final String... acceptedPrefixes) { + //While we implement a cache in this class we also want to enable + //ByteBuddy's default caching mechanism as it will cache the more + //useful output of the parsing and introspection of such types. + super( new TypePool.CacheProvider.Simple() ); + this.acceptedPrefixes = acceptedPrefixes; + } + + private boolean isCoreClassName(final String name) { + for ( String acceptedPrefix : this.acceptedPrefixes ) { + if ( name.startsWith( acceptedPrefix ) ) { + return true; + } + } + return false; + } + + @Override + protected Resolution doDescribe(final String name) { + if ( isCoreClassName( name ) ) { + final Resolution resolution = resolutions.get( name ); + if ( resolution != null ) { + return resolution; + } + else { + //We implement this additional layer of caching, which is on top of + //ByteBuddy's default caching, so as to prevent resolving the same + //types concurrently from the classloader. + //This is merely an efficiency improvement and will NOT provide a + //strict guarantee of symbols being resolved exactly once as there + //is no SPI within ByteBuddy which would allow this: the point is to + //make it exceptionally infrequent, which greatly helps with + //processing of large models. + return resolutions.computeIfAbsent( name, this::actualResolve ); + } + } + else { + //These are not cached to not leak references to application code names + return new Resolution.Illegal( name ); + } + } + + private Resolution actualResolve(final String name) { + try { + final Class aClass = Class.forName( name, false, hibernateClassLoader ); + return new TypePool.Resolution.Simple( TypeDescription.ForLoadedType.of( aClass ) ); + } + catch ( ClassNotFoundException e ) { + return new Resolution.Illegal( name ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassLocator.java new file mode 100644 index 000000000000..8724026ca5aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassLocator.java @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * Extends the TypePool contract of ByteBuddy with our additional needs. + */ +public interface EnhancerClassLocator extends TypePool { + + /** + * Register a new class to the locator explicitly. + * @param className + * @param originalBytes + */ + void registerClassNameAndBytes(String className, byte[] originalBytes); + + /** + * This can optionally be used to remove an explicit mapping when it's no longer + * essential to retain it. + * The underlying implementation might ignore the operation. + * @param className + */ + void deregisterClassNameAndBytes(String className); + + /** + * @return the underlying {@link ClassFileLocator} + */ + ClassFileLocator asClassFileLocator(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index 715100ff8a91..386e1574a64c 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -6,7 +6,6 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -14,8 +13,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.hibernate.Version; @@ -66,7 +65,6 @@ import net.bytebuddy.implementation.FixedValue; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.StubMethod; -import net.bytebuddy.pool.TypePool; import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; @@ -93,9 +91,7 @@ public Class annotationType() { protected final ByteBuddyEnhancementContext enhancementContext; private final ByteBuddyState byteBuddyState; - - private final EnhancerClassFileLocator classFileLocator; - private final TypePool typePool; + private final EnhancerClassLocator typePool; /** * Extract the following constants so that enhancement on large projects @@ -126,10 +122,20 @@ public Class annotationType() { * @param byteBuddyState refers to the ByteBuddy instance to use */ public EnhancerImpl(final EnhancementContext enhancementContext, final ByteBuddyState byteBuddyState) { + this( enhancementContext, byteBuddyState, ModelTypePool.buildModelTypePool( enhancementContext.getLoadingClassLoader() ) ); + } + + /** + * Expert level constructor, this allows for more control of state and bytecode loading, + * which allows integrators to optimise for particular contexts of use. + * @param enhancementContext + * @param byteBuddyState + * @param classLocator + */ + public EnhancerImpl(final EnhancementContext enhancementContext, final ByteBuddyState byteBuddyState, final EnhancerClassLocator classLocator) { this.enhancementContext = new ByteBuddyEnhancementContext( enhancementContext ); - this.byteBuddyState = byteBuddyState; - this.classFileLocator = new EnhancerClassFileLocator( enhancementContext.getLoadingClassLoader() ); - this.typePool = buildTypePool( classFileLocator ); + this.byteBuddyState = Objects.requireNonNull( byteBuddyState ); + this.typePool = Objects.requireNonNull( classLocator ); } /** @@ -147,13 +153,13 @@ public EnhancerImpl(final EnhancementContext enhancementContext, final ByteBuddy public byte[] enhance(String className, byte[] originalBytes) throws EnhancementException { //Classpool#describe does not accept '/' in the description name as it expects a class name. See HHH-12545 final String safeClassName = className.replace( '/', '.' ); - classFileLocator.registerClassNameAndBytes( safeClassName, originalBytes ); + typePool.registerClassNameAndBytes( safeClassName, originalBytes ); try { final TypeDescription typeDescription = typePool.describe( safeClassName ).resolve(); return byteBuddyState.rewrite( typePool, safeClassName, byteBuddy -> doEnhance( () -> byteBuddy.ignore( isDefaultFinalizer() ) - .redefine( typeDescription, ClassFileLocator.Simple.of( safeClassName, originalBytes ) ) + .redefine( typeDescription, typePool.asClassFileLocator() ) .annotateType( HIBERNATE_VERSION_ANNOTATION ), typeDescription ) ); @@ -165,14 +171,14 @@ public byte[] enhance(String className, byte[] originalBytes) throws Enhancement throw new EnhancementException( "Failed to enhance class " + className, e ); } finally { - classFileLocator.deregisterClassNameAndBytes( safeClassName ); + typePool.deregisterClassNameAndBytes( safeClassName ); } } @Override public void discoverTypes(String className, byte[] originalBytes) { if ( originalBytes != null ) { - classFileLocator.registerClassNameAndBytes( className, originalBytes ); + typePool.registerClassNameAndBytes( className, originalBytes ); } try { final TypeDescription typeDescription = typePool.describe( className ).resolve(); @@ -183,14 +189,10 @@ public void discoverTypes(String className, byte[] originalBytes) { throw new EnhancementException( "Failed to discover types for class " + className, e ); } finally { - classFileLocator.deregisterClassNameAndBytes( className ); + typePool.deregisterClassNameAndBytes( className ); } } - private TypePool buildTypePool(final ClassFileLocator classFileLocator) { - return TypePool.Default.WithLazyResolution.of( classFileLocator ); - } - private DynamicType.Builder doEnhance(Supplier> builderSupplier, TypeDescription managedCtClass) { // can't effectively enhance interfaces if ( managedCtClass.isInterface() ) { @@ -652,39 +654,4 @@ else if ( access != null && access.load().value() == AccessType.FIELD ) { } } - private static class EnhancerClassFileLocator extends ClassFileLocator.ForClassLoader { - private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); - - /** - * Creates a new class file locator for the given class loader. - * - * @param classLoader The class loader to query which must not be the bootstrap class loader, i.e. {@code null}. - */ - protected EnhancerClassFileLocator(ClassLoader classLoader) { - super( classLoader ); - } - - @Override - public Resolution locate(String className) throws IOException { - assert className != null; - final Resolution resolution = resolutions.get( className ); - if ( resolution != null ) { - return resolution; - } - else { - return super.locate( className ); - } - } - - void registerClassNameAndBytes(String className, byte[] bytes) { - assert className != null; - assert bytes != null; - resolutions.put( className, new Resolution.Explicit( bytes ) ); - } - - void deregisterClassNameAndBytes(String className) { - resolutions.remove( className ); - } - } - } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java new file mode 100644 index 000000000000..ecbc1ede0fea --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java @@ -0,0 +1,108 @@ +/* + * 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 . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * A TypePool suitable for loading user's classes, + * potentially in parallel operations. + */ +public class ModelTypePool extends TypePool.Default implements EnhancerClassLocator { + + private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); + private final OverridingClassFileLocator locator; + + private ModelTypePool(CacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) { + super( cacheProvider, classFileLocator, ReaderMode.FAST, parent ); + this.locator = classFileLocator; + } + + /** + * Creates a new empty EnhancerClassLocator instance which will load any application + * classes that need being reflected on from the ClassLoader passed as parameter. + * This TypePool will delegate, parent first, to a newly constructed empty instance + * of CoreTypePool; this parent pool will be used to load non-application types from + * the Hibernate classloader instead, not the one specified as argument. + * @see CoreTypePool + * @param classLoader + * @return the newly created EnhancerClassLocator + */ + public static EnhancerClassLocator buildModelTypePool(ClassLoader classLoader) { + return buildModelTypePool( ClassFileLocator.ForClassLoader.of( classLoader ) ); + } + + /** + * Similar to {@link #buildModelTypePool(ClassLoader)} except the application classes + * are not necessarily sourced from a standard classloader: it accepts a {@link ClassFileLocator}, + * which offers some more flexibility. + * @param classFileLocator + * @return the newly created EnhancerClassLocator + */ + public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator) { + return buildModelTypePool( classFileLocator, new CoreTypePool() ); + } + + /** + * Similar to {@link #buildModelTypePool(ClassFileLocator)} but allows specifying an existing + * {@link CoreTypePool} to be used as parent pool. + * This forms allows constructing a custom CoreTypePool and also separated the cache of the parent pool, + * which might be useful to reuse for multiple enhancement processes while desiring a clean new + * state for the {@link ModelTypePool}. + * @param classFileLocator + * @param coreTypePool + * @return + */ + public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) { + return buildModelTypePool( classFileLocator, coreTypePool, new TypePool.CacheProvider.Simple() ); + } + + /** + * The more advanced strategy to construct a new ModelTypePool, allowing customization of all its aspects. + * @param classFileLocator + * @param coreTypePool + * @param cacheProvider + * @return + */ + public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, CacheProvider cacheProvider) { + Objects.requireNonNull( classFileLocator ); + Objects.requireNonNull( coreTypePool ); + Objects.requireNonNull( cacheProvider ); + return new ModelTypePool( cacheProvider, new OverridingClassFileLocator( classFileLocator ), coreTypePool ); + } + + @Override + protected Resolution doDescribe(final String name) { + final Resolution resolution = resolutions.get( name ); + if ( resolution != null ) { + return resolution; + } + else { + return resolutions.computeIfAbsent( name, super::doDescribe ); + } + } + + @Override + public void registerClassNameAndBytes(final String className, final byte[] bytes) { + locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) ); + } + + @Override + public void deregisterClassNameAndBytes(final String className) { + locator.remove( className ); + } + + @Override + public ClassFileLocator asClassFileLocator() { + return locator; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java new file mode 100644 index 000000000000..2f64dabbdd70 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java @@ -0,0 +1,52 @@ +/* + * 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 . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import net.bytebuddy.dynamic.ClassFileLocator; + +/** + * Allows wrapping another ClassFileLocator to add the ability to define + * resolution overrides for specific resources. + */ +public final class OverridingClassFileLocator implements ClassFileLocator { + + private final ConcurrentHashMap registeredResolutions = new ConcurrentHashMap<>(); + private final ClassFileLocator parent; + + public OverridingClassFileLocator(final ClassFileLocator parent) { + this.parent = Objects.requireNonNull( parent ); + } + + @Override + public Resolution locate(final String name) throws IOException { + final Resolution resolution = registeredResolutions.get( name ); + if ( resolution != null ) { + return resolution; + } + else { + return parent.locate( name ); + } + } + + @Override + public void close() throws IOException { + //Nothing to do: we're not responsible for parent + } + + void put(String className, Resolution.Explicit explicit) { + registeredResolutions.put( className, explicit ); + } + + void remove(String className) { + registeredResolutions.remove( className ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java index d5088c0424db..392793881c6d 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BytecodeProviderImpl.java @@ -21,6 +21,7 @@ import java.util.concurrent.Callable; import org.hibernate.HibernateException; +import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerClassLocator; import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImpl; import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.Enhancer; @@ -67,6 +68,7 @@ import net.bytebuddy.jar.asm.Type; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.pool.TypePool; import org.checkerframework.checker.nullness.qual.Nullable; public class BytecodeProviderImpl implements BytecodeProvider { @@ -1312,6 +1314,18 @@ public String[] call() { return new EnhancerImpl( enhancementContext, byteBuddyState ); } + /** + * Similar to {@link #getEnhancer(EnhancementContext)} but intended for advanced users who wish + * to customize how ByteBuddy is locating the class files and caching the types. + * Possibly used in Quarkus in a future version. + * @param enhancementContext + * @param classLocator + * @return + */ + public @Nullable Enhancer getEnhancer(EnhancementContext enhancementContext, EnhancerClassLocator classLocator) { + return new EnhancerImpl( enhancementContext, byteBuddyState, classLocator ); + } + @Override public void resetCaches() { byteBuddyState.clearState();