diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java new file mode 100644 index 000000000000..6a1d2cb1082b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * A simple cache provider that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerCacheProvider extends TypePool.CacheProvider.Simple { + + private final ThreadLocal enhancementState = new ThreadLocal<>(); + + @Override + public TypePool.Resolution find(final String name) { + final EnhancementState enhancementState = getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getTypePoolResolution(); + } + return super.find( name ); + } + + EnhancementState getEnhancementState() { + return enhancementState.get(); + } + + void setEnhancementState(EnhancementState state) { + enhancementState.set( state ); + } + + void removeEnhancementState() { + enhancementState.remove(); + } + + static final class EnhancementState { + private final String className; + private final ClassFileLocator.Resolution classFileResolution; + private TypePool.Resolution typePoolResolution; + + public EnhancementState(String className, ClassFileLocator.Resolution classFileResolution) { + this.className = className; + this.classFileResolution = classFileResolution; + } + + public String getClassName() { + return className; + } + + public ClassFileLocator.Resolution getClassFileResolution() { + return classFileResolution; + } + + public TypePool.Resolution getTypePoolResolution() { + return typePoolResolution; + } + + public void setTypePoolResolution(TypePool.Resolution typePoolResolution) { + this.typePoolResolution = typePoolResolution; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java new file mode 100644 index 000000000000..b3803f9c940d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; + +import java.io.IOException; + +/** + * A delegating ClassFileLocator that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerClassFileLocator implements ClassFileLocator { + + private final EnhancerCacheProvider cacheProvider; + private final ClassFileLocator delegate; + + public EnhancerClassFileLocator(EnhancerCacheProvider cacheProvider, ClassFileLocator delegate) { + this.cacheProvider = cacheProvider; + this.delegate = delegate; + } + + @Override + public Resolution locate(final String name) throws IOException { + final EnhancerCacheProvider.EnhancementState enhancementState = cacheProvider.getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getClassFileResolution(); + } + return delegate.locate( name ); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + +} 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 index 3900b8ebe777..a62095d8aad1 100644 --- 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 @@ -4,23 +4,21 @@ */ 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; +import java.util.Objects; + /** * 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 final SafeCacheProvider poolCache; + private final EnhancerClassFileLocator locator; + private final EnhancerCacheProvider poolCache; - private ModelTypePool(SafeCacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) { + private ModelTypePool(EnhancerCacheProvider cacheProvider, EnhancerClassFileLocator classFileLocator, CoreTypePool parent) { super( cacheProvider, classFileLocator, ReaderMode.FAST, parent ); this.poolCache = cacheProvider; this.locator = classFileLocator; @@ -62,7 +60,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @return */ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) { - return buildModelTypePool( classFileLocator, coreTypePool, new SafeCacheProvider() ); + return buildModelTypePool( classFileLocator, coreTypePool, new EnhancerCacheProvider() ); } /** @@ -72,44 +70,35 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @param cacheProvider * @return */ - public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, SafeCacheProvider cacheProvider) { + static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, EnhancerCacheProvider 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 ); - } + return new ModelTypePool( cacheProvider, new EnhancerClassFileLocator( cacheProvider, classFileLocator ), coreTypePool ); } @Override public void registerClassNameAndBytes(final String className, final byte[] bytes) { - //Very important: ensure the registered override is actually effective in case this class - //was already resolved in the recent past; this could have happened for example as a side effect - //of symbol resolution during enhancement of a different class, or very simply when attempting - //to re-enhanced the same class - which happens frequently in WildFly because of the class transformers - //being triggered concurrently by multiple parallel deployments. - resolutions.remove( className ); - poolCache.remove( className ); - locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) ); + final EnhancerCacheProvider.EnhancementState currentEnhancementState = poolCache.getEnhancementState(); + if ( currentEnhancementState != null ) { + throw new IllegalStateException( "Re-entrant enhancement is not supported: " + className ); + } + final EnhancerCacheProvider.EnhancementState state = new EnhancerCacheProvider.EnhancementState( + className, + new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) + ); + // Set the state first because the ClassFileLocator needs this in the doDescribe() call below + poolCache.setEnhancementState( state ); + state.setTypePoolResolution( doDescribe( className ) ); } @Override - public void deregisterClassNameAndBytes(final String className) { - locator.remove( className ); + public void deregisterClassNameAndBytes(String className) { + poolCache.removeEnhancementState(); } @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 deleted file mode 100644 index 3ef974cf405c..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.bytecode.enhance.internal.bytebuddy; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Objects; - -import net.bytebuddy.dynamic.ClassFileLocator; - -/** - * Allows wrapping another ClassFileLocator to add the ability to define - * resolution overrides for specific resources. - * This is useful when enhancing entities and we need to process an - * input byte array representing the bytecode of the entity, and some - * external party is providing the byte array explicitly, avoiding for - * us having to load it from the classloader. - * We'll still need to load several other symbols from a parent @{@link ClassFileLocator} - * (typically based on the classloader), but need to return the provided - * byte array for some selected resources. - * Any override is scoped to the current thread; this is to avoid - * interference among threads in systems which perform parallel - * class enhancement, for example containers requiring "on the fly" - * transformation of classes as they are being loaded will often - * perform such operations concurrently. - */ -final class OverridingClassFileLocator implements ClassFileLocator { - - private final ThreadLocal> registeredResolutions = ThreadLocal.withInitial( () -> new HashMap<>() ); - private final ClassFileLocator parent; - - /** - * @param parent the @{@link ClassFileLocator} which will be used to load any resource which wasn't explicitly registered as an override. - */ - OverridingClassFileLocator(final ClassFileLocator parent) { - this.parent = Objects.requireNonNull( parent ); - } - - @Override - public Resolution locate(final String name) throws IOException { - final Resolution resolution = getLocalMap().get( name ); - if ( resolution != null ) { - return resolution; - } - else { - return parent.locate( name ); - } - } - - private HashMap getLocalMap() { - return registeredResolutions.get(); - } - - @Override - public void close() { - registeredResolutions.remove(); - } - - /** - * Registers an explicit resolution override - * - * @param className - * @param explicit - */ - void put(String className, Resolution.Explicit explicit) { - getLocalMap().put( className, explicit ); - } - - /** - * Removes an explicit resolution override - * - * @param className - */ - void remove(String className) { - getLocalMap().remove( className ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/SafeCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/SafeCacheProvider.java deleted file mode 100644 index 748a92cb6275..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/SafeCacheProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.bytecode.enhance.internal.bytebuddy; - -import java.util.HashMap; -import java.util.Map; - -import net.bytebuddy.pool.TypePool; - -/** - * An implementation of @{@link net.bytebuddy.pool.TypePool.CacheProvider} which scopes - * all state to the current thread, and allows to remove specific registrations. - * The threadscoping is necessary to resolve a race condition happening during concurrent entity enhancement: - * while one thread is resolving metadata about the entity which needs to be enhanced, other threads - * might be working on the same operation (or a different entity which needs this one to be resolved) - * and the resolution output - potentially shared across them - could be tainted as they do need - * to occasionally work on different input because of the specific overrides managed via @{@link OverridingClassFileLocator}. - */ -final class SafeCacheProvider implements TypePool.CacheProvider { - - private final ThreadLocal> delegate = ThreadLocal.withInitial( () -> new HashMap<>() ); - - @Override - public TypePool.Resolution find(final String name) { - return delegate.get().get( name ); - } - - @Override - public TypePool.Resolution register(final String name, final TypePool.Resolution resolution) { - final TypePool.Resolution cached = delegate.get().putIfAbsent( name, resolution ); - return cached == null - ? resolution - : cached; - } - - @Override - public void clear() { - delegate.get().clear(); - delegate.remove(); - } - - public TypePool.Resolution remove(final String name) { - return delegate.get().remove( name ); - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java index 8c66170b1f53..10c114fdf09f 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/BasicProxyFactoryImpl.java @@ -13,8 +13,6 @@ import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.proxy.ProxyConfiguration; -import net.bytebuddy.NamingStrategy; -import net.bytebuddy.TypeCache; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; @@ -37,13 +35,12 @@ public BasicProxyFactoryImpl(final Class superClass, final Class interfaceClass, } final Class superClassOrMainInterface = superClass != null ? superClass : interfaceClass; - final TypeCache.SimpleKey cacheKey = new TypeCache.SimpleKey( superClassOrMainInterface ); + final ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); + final String proxyClassName = superClassOrMainInterface.getName() + "$" + PROXY_NAMING_SUFFIX; - ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); - - this.proxyClass = byteBuddyState.loadBasicProxy( superClassOrMainInterface, cacheKey, byteBuddy -> + this.proxyClass = byteBuddyState.loadBasicProxy( superClassOrMainInterface, proxyClassName, (byteBuddy, namingStrategy) -> helpers.appendIgnoreAlsoAtEnd( byteBuddy - .with( new NamingStrategy.SuffixingRandom( PROXY_NAMING_SUFFIX, new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( superClassOrMainInterface.getName() ) ) ) + .with( namingStrategy ) .subclass( superClass == null ? Object.class : superClass, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR ) .implement( interfaceClass == null ? NO_INTERFACES : new Class[]{ interfaceClass } ) .defineField( ProxyConfiguration.INTERCEPTOR_FIELD_NAME, ProxyConfiguration.Interceptor.class, Visibility.PRIVATE ) diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java index 9ea78c6076b0..9c5871b1d1f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/internal/bytebuddy/ByteBuddyState.java @@ -10,8 +10,11 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; import java.util.function.Function; +import net.bytebuddy.NamingStrategy; +import net.bytebuddy.description.type.TypeDescription; import org.hibernate.HibernateException; import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImplConstants; import org.hibernate.bytecode.enhance.spi.EnhancerConstants; @@ -94,7 +97,9 @@ public ByteBuddyState() { * @param cacheKey The cache key. * @param makeProxyFunction A function building the proxy. * @return The loaded proxy class. + * @deprecated Use {@link #loadProxy(Class, String, BiFunction)} instead. */ + @Deprecated(forRemoval = true, since = "6.6") public Class loadProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return load( referenceClass, proxyCache, cacheKey, makeProxyFunction ); @@ -107,12 +112,40 @@ public Class loadProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, * @param cacheKey The cache key. * @param makeProxyFunction A function building the proxy. * @return The loaded proxy class. + * @deprecated Use {@link #loadBasicProxy(Class, String, BiFunction)} instead. */ + @Deprecated(forRemoval = true, since = "6.6") Class loadBasicProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return load( referenceClass, basicProxyCache, cacheKey, makeProxyFunction ); } + /** + * Load a proxy as generated by the {@link ProxyFactory}. + * + * @param referenceClass The main class to proxy - might be an interface. + * @param proxyClassName The proxy class name. + * @param makeProxyFunction A function building the proxy. + * @return The loaded proxy class. + */ + public Class loadProxy(Class referenceClass, String proxyClassName, + BiFunction> makeProxyFunction) { + return load( referenceClass, proxyClassName, makeProxyFunction ); + } + + /** + * Load a proxy as generated by the {@link BasicProxyFactory}. + * + * @param referenceClass The main class to proxy - might be an interface. + * @param proxyClassName The proxy class name. + * @param makeProxyFunction A function building the proxy. + * @return The loaded proxy class. + */ + Class loadBasicProxy(Class referenceClass, String proxyClassName, + BiFunction> makeProxyFunction) { + return load( referenceClass, proxyClassName, makeProxyFunction ); + } + /** * Load a class generated by ByteBuddy. * @@ -121,18 +154,9 @@ Class loadBasicProxy(Class referenceClass, TypeCache.SimpleKey cacheKey, * @return The loaded generated class. */ public Class load(Class referenceClass, Function> makeClassFunction) { - Unloaded result = - make( makeClassFunction.apply( byteBuddy ) ); - if (DEBUG) { - try { - result.saveIn( new File( System.getProperty( "java.io.tmpdir" ) + "/bytebuddy/" ) ); - } - catch (IOException e) { - LOG.warn( "Unable to save generated class %1$s", result.getTypeDescription().getName(), e ); - } - } - return result.load( referenceClass.getClassLoader(), resolveClassLoadingStrategy( referenceClass ) ) - .getLoaded(); + return make( makeClassFunction.apply( byteBuddy ) ) + .load( referenceClass.getClassLoader(), resolveClassLoadingStrategy( referenceClass ) ) + .getLoaded(); } /** @@ -180,6 +204,39 @@ void clearState() { basicProxyCache.clear(); } + /** + * Load a class generated by ByteBuddy. + * + * @param referenceClass The main class for which to create a class - might be an interface. + * @param className The name under which the class shall be created. + * @param makeClassFunction A function building the class. + * @return The loaded generated class. + */ + public Class load(Class referenceClass, String className, BiFunction> makeClassFunction) { + try { + return referenceClass.getClassLoader().loadClass( className ); + } + catch (ClassNotFoundException e) { + // Ignore + } + try { + return make( makeClassFunction.apply( byteBuddy, new FixedNamingStrategy( className ) ) ) + .load( + referenceClass.getClassLoader(), + resolveClassLoadingStrategy( referenceClass ) + ) + .getLoaded(); + } + catch (LinkageError e) { + try { + return referenceClass.getClassLoader().loadClass( className ); + } + catch (ClassNotFoundException ex) { + throw new RuntimeException( "Couldn't load or define class [" + className + "]", e ); + } + } + } + private Class load(Class referenceClass, TypeCache cache, TypeCache.SimpleKey cacheKey, Function> makeProxyFunction) { return cache.findOrInsert( @@ -325,4 +382,16 @@ private static ClassLoadingStrategy resolveClassLoadingStrategy(Cla } } + private static class FixedNamingStrategy extends NamingStrategy.AbstractBase { + private final String className; + + public FixedNamingStrategy(String className) { + this.className = className; + } + + @Override + protected String name(TypeDescription typeDescription) { + return 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 855dffc79fec..74cf419e8495 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 @@ -10,6 +10,7 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -146,11 +147,9 @@ public ReflectionOptimizer getReflectionOptimizer( fastClass = null; } else { - fastClass = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - INSTANTIATOR_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) + final String className = clazz.getName() + "$" + INSTANTIATOR_PROXY_NAMING_SUFFIX; + fastClass = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) .subclass( ReflectionOptimizer.InstantiationOptimizer.class ) .method( newInstanceMethodName ) .intercept( MethodCall.construct( constructor ) ) @@ -210,11 +209,9 @@ public ReflectionOptimizer getReflectionOptimizer( fastClass = null; } else { - fastClass = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - INSTANTIATOR_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) + final String className = clazz.getName() + "$" + INSTANTIATOR_PROXY_NAMING_SUFFIX; + fastClass = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) .subclass( ReflectionOptimizer.InstantiationOptimizer.class ) .method( newInstanceMethodName ) .intercept( MethodCall.construct( constructor ) ) @@ -235,23 +232,41 @@ public ReflectionOptimizer getReflectionOptimizer( return null; } - Class superClass = determineAccessOptimizerSuperClass( clazz, getters, setters ); - final String[] propertyNames = propertyAccessMap.keySet().toArray( new String[0] ); - final Class bulkAccessor = byteBuddyState.load( clazz, byteBuddy -> byteBuddy - .with( new NamingStrategy.SuffixingRandom( - OPTIMIZER_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) - ) ) - .subclass( superClass ) - .implement( ReflectionOptimizer.AccessOptimizer.class ) - .method( getPropertyValuesMethodName ) - .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) - .method( setPropertyValuesMethodName ) - .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) - .method( getPropertyNamesMethodName ) - .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) - ); + final Class superClass = determineAccessOptimizerSuperClass( clazz, propertyNames, getters, setters ); + + final String className = clazz.getName() + "$" + OPTIMIZER_PROXY_NAMING_SUFFIX + encodeName( propertyNames, getters, setters ); + final Class bulkAccessor; + if ( className.getBytes( StandardCharsets.UTF_8 ).length >= 0x10000 ) { + // The JVM has a 64K byte limit on class name length, so fallback to random name if encoding exceeds that + bulkAccessor = byteBuddyState.load( clazz, byteBuddy -> byteBuddy + .with( new NamingStrategy.SuffixingRandom( + OPTIMIZER_PROXY_NAMING_SUFFIX, + new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( clazz.getName() ) + ) ) + .subclass( superClass ) + .implement( ReflectionOptimizer.AccessOptimizer.class ) + .method( getPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) + .method( setPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) + .method( getPropertyNamesMethodName ) + .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) + ); + } + else { + bulkAccessor = byteBuddyState.load( clazz, className, (byteBuddy, namingStrategy) -> byteBuddy + .with( namingStrategy ) + .subclass( superClass ) + .implement( ReflectionOptimizer.AccessOptimizer.class ) + .method( getPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new GetPropertyValues( clazz, propertyNames, getters ) ) ) + .method( setPropertyValuesMethodName ) + .intercept( new Implementation.Simple( new SetPropertyValues( clazz, propertyNames, setters ) ) ) + .method( getPropertyNamesMethodName ) + .intercept( MethodCall.call( new CloningPropertyCall( propertyNames ) ) ) + ); + } try { return new ReflectionOptimizerImpl( @@ -266,6 +281,7 @@ public ReflectionOptimizer getReflectionOptimizer( private static class ForeignPackageClassInfo { final Class clazz; + final List propertyNames = new ArrayList<>(); final List getters = new ArrayList<>(); final List setters = new ArrayList<>(); @@ -274,7 +290,7 @@ public ForeignPackageClassInfo(Class clazz) { } } - private Class determineAccessOptimizerSuperClass(Class clazz, Member[] getters, Member[] setters) { + private Class determineAccessOptimizerSuperClass(Class clazz, String[] propertyNames, Member[] getters, Member[] setters) { if ( clazz.isInterface() ) { return Object.class; } @@ -288,11 +304,17 @@ private Class determineAccessOptimizerSuperClass(Class clazz, Member[] get for ( int i = 0; i < getters.length; i++ ) { final Member getter = getters[i]; final Member setter = setters[i]; + boolean found = false; if ( getter.getDeclaringClass() == foreignPackageClassInfo.clazz && !Modifier.isPublic( getter.getModifiers() ) ) { foreignPackageClassInfo.getters.add( getter ); + found = true; } if ( setter.getDeclaringClass() == foreignPackageClassInfo.clazz && !Modifier.isPublic( setter.getModifiers() ) ) { foreignPackageClassInfo.setters.add( setter ); + found = true; + } + if ( found ) { + foreignPackageClassInfo.propertyNames.add( propertyNames[i] ); } } if ( foreignPackageClassInfo.getters.isEmpty() && foreignPackageClassInfo.setters.isEmpty() ) { @@ -304,16 +326,13 @@ private Class determineAccessOptimizerSuperClass(Class clazz, Member[] get for ( int i = foreignPackageClassInfos.size() - 1; i >= 0; i-- ) { final ForeignPackageClassInfo foreignPackageClassInfo = foreignPackageClassInfos.get( i ); final Class newSuperClass = superClass; + + final String className = foreignPackageClassInfo.clazz.getName() + "$" + OPTIMIZER_PROXY_NAMING_SUFFIX + encodeName( foreignPackageClassInfo.propertyNames, foreignPackageClassInfo.getters, foreignPackageClassInfo.setters ); superClass = byteBuddyState.load( foreignPackageClassInfo.clazz, - byteBuddy -> { - DynamicType.Builder builder = byteBuddy.with( - new NamingStrategy.SuffixingRandom( - OPTIMIZER_PROXY_NAMING_SUFFIX, - new NamingStrategy.SuffixingRandom.BaseNameResolver.ForFixedValue( - foreignPackageClassInfo.clazz.getName() ) - ) - ).subclass( newSuperClass ); + className, + (byteBuddy, namingStrategy) -> { + DynamicType.Builder builder = byteBuddy.with( namingStrategy ).subclass( newSuperClass ); for ( Member getter : foreignPackageClassInfo.getters ) { final Class getterType; if ( getter instanceof Field ) { @@ -383,6 +402,42 @@ private Class determineAccessOptimizerSuperClass(Class clazz, Member[] get return superClass; } + private static String encodeName(String[] propertyNames, Member[] getters, Member[] setters) { + return encodeName( Arrays.asList( propertyNames ), Arrays.asList( getters ), Arrays.asList( setters ) ); + } + + private static String encodeName(List propertyNames, List getters, List setters) { + final StringBuilder sb = new StringBuilder(); + for ( int i = 0; i < propertyNames.size(); i++ ) { + final String propertyName = propertyNames.get( i ); + final Member getter = getters.get( i ); + final Member setter = setters.get( i ); + // Encode the two member types as 4 bit integer encoded as hex character + sb.append( Integer.toHexString( getKind( getter ) << 2 | getKind( setter ) ) ); + sb.append( propertyName ); + } + return sb.toString(); + } + + private static int getKind(Member member) { + // Encode the member type as 2 bit integer + if ( member == EMBEDDED_MEMBER ) { + return 0; + } + else if ( member instanceof Field ) { + return 1; + } + else if ( member instanceof Method ) { + return 2; + } + else if ( member instanceof ForeignPackageMember ) { + return 3; + } + else { + throw new IllegalArgumentException( "Unknown member type: " + member ); + } + } + private static class ForeignPackageMember implements Member { private final Class foreignPackageAccessor; diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java index 1a156bb7a907..5df38448cc0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java @@ -60,9 +60,6 @@ public byte[] transform( catch (final Exception e) { throw new TransformerException( "Error performing enhancement of " + className, e ); } - finally { - bytecodeProvider.resetCaches(); - } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java index 97c754f57e34..8d3767b2c1f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import org.hibernate.HibernateException; @@ -22,7 +23,6 @@ import net.bytebuddy.ByteBuddy; import net.bytebuddy.NamingStrategy; -import net.bytebuddy.TypeCache; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; @@ -53,7 +53,8 @@ public Class buildProxy( } Collections.addAll( key, interfaces ); - return byteBuddyState.loadProxy( persistentClass, new TypeCache.SimpleKey( key ), + final String proxyClassName = persistentClass.getTypeName() + "$" + PROXY_NAMING_SUFFIX; + return byteBuddyState.loadProxy( persistentClass, proxyClassName, proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -62,7 +63,7 @@ public Class buildProxy( */ @Deprecated public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass, final Class[] interfaces) { - return byteBuddyState.make( proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), + return byteBuddyState.make( proxyBuilderLegacy( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -71,15 +72,24 @@ public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass */ public DynamicType.Unloaded buildUnloadedProxy(TypePool typePool, TypeDefinition persistentClass, Collection interfaces) { - return byteBuddyState.make( typePool, proxyBuilder( persistentClass, interfaces ) ); + return byteBuddyState.make( typePool, proxyBuilderLegacy( persistentClass, interfaces ) ); } - private Function> proxyBuilder(TypeDefinition persistentClass, + private Function> proxyBuilderLegacy(TypeDefinition persistentClass, + Collection interfaces) { + final BiFunction> proxyBuilder = + proxyBuilder( persistentClass, interfaces ); + final NamingStrategy.Suffixing namingStrategy = + new NamingStrategy.Suffixing( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ); + return byteBuddy -> proxyBuilder.apply( byteBuddy, namingStrategy ); + } + + private BiFunction> proxyBuilder(TypeDefinition persistentClass, Collection interfaces) { ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); - return byteBuddy -> helpers.appendIgnoreAlsoAtEnd( byteBuddy + return (byteBuddy, namingStrategy) -> helpers.appendIgnoreAlsoAtEnd( byteBuddy .ignore( helpers.getGroovyGetMetaClassFilter() ) - .with( new NamingStrategy.SuffixingRandom( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ) ) + .with( namingStrategy ) .subclass( interfaces.size() == 1 ? persistentClass : OBJECT, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING ) .implement( interfaces ) .method( helpers.getVirtualNotFinalizerFilter() ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java new file mode 100644 index 000000000000..142f0376b50f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java @@ -0,0 +1,191 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.proxy; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.boot.MetadataSources; +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.TcclLookupPrecedence; +import org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl; +import org.hibernate.bytecode.spi.ByteCodeHelper; +import org.hibernate.bytecode.spi.BytecodeProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.Set; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Jira("https://hibernate.atlassian.net/browse/HHH-14694") +public class ProxyClassReuseTest { + + @Test + void testReuse() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + final BytecodeProvider bytecodeProvider = new BytecodeProviderImpl(); + Class proxyClass1 = withFactory( proxyGetter, bytecodeProvider, null ); + Class proxyClass2 = withFactory( proxyGetter, bytecodeProvider, null ); + assertSame( proxyClass1, proxyClass2 ); + } + + @Test + void testReuseWithDifferentFactories() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + Class proxyClass1 = withFactory( proxyGetter, null, null ); + Class proxyClass2 = withFactory( proxyGetter, null, null ); + assertSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), MyEntity.class.getClassLoader() ); + assertSame( proxyClass2.getClassLoader(), MyEntity.class.getClassLoader() ); + } + + @Test + void testNoReuse() { + Function> proxyGetter = sf -> { + try { + //noinspection unchecked + Function> getter = (Function>) Thread.currentThread() + .getContextClassLoader() + .loadClass( ProxyClassReuseTest.class.getName() + "$ProxyGetter" ) + .getConstructor() + .newInstance(); + return getter.apply( sf ); + } + catch (Exception ex) { + throw new RuntimeException( ex ); + } + }; + // Create two isolated class loaders that load the entity and proxy classes in isolation + Set isolatedClasses = Set.of( "org.hibernate.orm.test.proxy.*" ); + ClassLoader cl1 = new IsolatingClassLoader( isolatedClasses ); + ClassLoader cl2 = new IsolatingClassLoader( isolatedClasses ); + Class proxyClass1 = withFactory( proxyGetter, null, cl1 ); + Class proxyClass2 = withFactory( proxyGetter, null, cl2 ); + // The two proxy classes shall be defined on the respective isolated class loaders and hence be different + assertNotSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), cl1 ); + assertSame( proxyClass2.getClassLoader(), cl2 ); + } + + T withFactory(Function consumer, BytecodeProvider bytecodeProvider, ClassLoader classLoader) { + final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( classLoader ); + } + final BootstrapServiceRegistryBuilder bsr = new BootstrapServiceRegistryBuilder(); + bsr.applyTcclLookupPrecedence( TcclLookupPrecedence.BEFORE ); + final StandardServiceRegistryBuilder builder = ServiceRegistryUtil.serviceRegistryBuilder(bsr.build()); + if ( bytecodeProvider != null ) { + builder.addService( BytecodeProvider.class, bytecodeProvider ); + } + final StandardServiceRegistry ssr = builder.build(); + + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) new MetadataSources( ssr ) + .addAnnotatedClassName( ProxyClassReuseTest.class.getName() + "$MyEntity" ) + .buildMetadata() + .getSessionFactoryBuilder() + .build()) { + return consumer.apply( sf ); + } + catch (Exception e) { + StandardServiceRegistryBuilder.destroy( ssr ); + throw e; + } + } + finally { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( oldClassLoader ); + } + } + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @Id + String id; + String name; + } + + public static class ProxyGetter implements Function> { + @Override + public Class apply(SessionFactoryImplementor sf) { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + } + } + + private static class IsolatingClassLoader extends ClassLoader { + + private final Set isolatedClasses; + + public IsolatingClassLoader(Set isolatedClasses) { + super( IsolatingClassLoader.class.getClassLoader() ); + this.isolatedClasses = isolatedClasses; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + if (isIsolatedClass(name)) { + InputStream is = this.getResourceAsStream( name.replace( '.', '/' ) + ".class" ); + if ( is == null ) { + throw new ClassNotFoundException( name + " not found" ); + } + + try { + byte[] bytecode = ByteCodeHelper.readByteCode( is ); + return defineClass( name, bytecode, 0, bytecode.length ); + } + catch( Throwable t ) { + throw new ClassNotFoundException( name + " not found", t ); + } + } else { + // Parent first + c = super.loadClass(name, resolve); + } + } + return c; + } + } + + private boolean isIsolatedClass(String name) { + if (isolatedClasses != null) { + for (String isolated : isolatedClasses) { + if (isolated.endsWith(".*")) { + String isolatedPackage = isolated.substring(0, isolated.length() - 1); + String paramPackage = name.substring(0, name.lastIndexOf('.') + 1); + if (paramPackage.startsWith(isolatedPackage)) { + // Matching package + return true; + } + } else if (isolated.equals(name)) { + return true; + } + } + } + return false; + } + } +}