Skip to content

Commit 332112d

Browse files
committed
HHH-18901 The entity enhancement process made defensive against concurrent enhancement
In particular, made it defensive against concurrent enhancement of the same resource, or of resources that might trigger loading symbols which another thread is re-processing.
1 parent a9e56ff commit 332112d

File tree

10 files changed

+279
-50
lines changed

10 files changed

+279
-50
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.bytecode.enhance.internal.bytebuddy;
8+
9+
import java.util.Objects;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.ConcurrentMap;
12+
13+
import net.bytebuddy.description.type.TypeDescription;
14+
import net.bytebuddy.pool.TypePool;
15+
16+
/**
17+
* A CacheProvider for ByteBuddy which is specifically designed for
18+
* our CoreTypePool: it ensures the cache content doesn't get tainted
19+
* by model types or anything else which isn't responsibility of this
20+
* particular type pool.
21+
* @implNote This cache instance shares the same @{@link CorePrefixFilter}
22+
* instance of the @{@link CoreTypePool} which is using it, and uses it
23+
* to guard writes into its internal caches.
24+
*/
25+
class CoreCacheProvider implements TypePool.CacheProvider {
26+
27+
private final ConcurrentMap<String, TypePool.Resolution> storage = new ConcurrentHashMap<>();
28+
private final CorePrefixFilter acceptedPrefixes;
29+
30+
CoreCacheProvider(final CorePrefixFilter acceptedPrefixes) {
31+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
32+
register(
33+
Object.class.getName(),
34+
new TypePool.Resolution.Simple( TypeDescription.ForLoadedType.of( Object.class ) )
35+
);
36+
}
37+
38+
/**
39+
* {@inheritDoc}
40+
*/
41+
@Override
42+
public TypePool.Resolution find(final String name) {
43+
return storage.get( name );
44+
}
45+
46+
/**
47+
* {@inheritDoc}
48+
*/
49+
@Override
50+
public TypePool.Resolution register(String name, TypePool.Resolution resolution) {
51+
//Ensure we DO NOT cache anything from a non-core namespace, to not leak application specific code:
52+
if ( acceptedPrefixes.isCoreClassName( name ) ) {
53+
TypePool.Resolution cached = storage.putIfAbsent( name, resolution );
54+
return cached == null
55+
? resolution
56+
: cached;
57+
}
58+
else {
59+
return resolution;
60+
}
61+
}
62+
63+
/**
64+
* {@inheritDoc}
65+
*/
66+
@Override
67+
public void clear() {
68+
storage.clear();
69+
}
70+
71+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.bytecode.enhance.internal.bytebuddy;
8+
9+
import java.util.Objects;
10+
11+
/**
12+
* We differentiate between core classes and application classes during symbol
13+
* resolution for the purposes of entity enhancement.
14+
* The discriminator is the prefix of the fully qualified classname, for
15+
* example it could be package names.
16+
* The "core classes" don't have to be comprehensively defined: we want a small
17+
* set of prefixes for which we know with certainty that a)They won't be used
18+
* in application code (assuming people honour reasonable package name rules)
19+
* or any code that needs being enhanced. and b) frequently need to be looked up
20+
* during the enhancement process.
21+
* A great example is the "jakarta.persistence.Entity" annotation: we'll most likely
22+
* need to load it when doing any form of introspection on user's code, but we expect
23+
* the bytecode which represents the annotation to not be enhanced.
24+
* We then benefit from caching such representations of object types which are frequently
25+
* loaded; since caching end user code would lead to enhancement problems, it's best
26+
* to keep the list conservative when in doubt.
27+
* For example, don't consider all of {@code "org.hibernate."} prefixes as safe, as
28+
* that would include entities used during our own testsuite and entities defined by Envers.
29+
*
30+
*/
31+
public final class CorePrefixFilter {
32+
33+
private final String[] acceptedPrefixes;
34+
public static final CorePrefixFilter DEFAULT_INSTANCE = new CorePrefixFilter();
35+
36+
/**
37+
* Do not invoke: use DEFAULT_INSTANCE
38+
*/
39+
CorePrefixFilter() {
40+
//By default optimise for jakarta annotations, java util collections, and Hibernate marker interfaces
41+
this("jakarta.", "java.", "org.hibernate.annotations.", "org.hibernate.bytecode.enhance.spi.", "org.hibernate.engine.spi.");
42+
}
43+
44+
public CorePrefixFilter(final String... acceptedPrefixes) {
45+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
46+
}
47+
48+
public boolean isCoreClassName(final String name) {
49+
for ( String acceptedPrefix : this.acceptedPrefixes ) {
50+
if ( name.startsWith( acceptedPrefix ) ) {
51+
return true;
52+
}
53+
}
54+
return false;
55+
}
56+
57+
}

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.hibernate.bytecode.enhance.internal.bytebuddy;
88

9+
import java.util.Objects;
910
import java.util.concurrent.ConcurrentHashMap;
1011

1112
import net.bytebuddy.description.type.TypeDescription;
@@ -27,42 +28,39 @@ public class CoreTypePool extends TypePool.AbstractBase implements TypePool {
2728

2829
private final ClassLoader hibernateClassLoader = CoreTypePool.class.getClassLoader();
2930
private final ConcurrentHashMap<String, Resolution> resolutions = new ConcurrentHashMap<>();
30-
private final String[] acceptedPrefixes;
31+
private final CorePrefixFilter acceptedPrefixes;
3132

3233
/**
33-
* Construct a new {@link CoreTypePool} with its default configuration:
34-
* to only load classes whose package names start with either "jakarta."
35-
* or "java."
34+
* Construct a new {@link CoreTypePool} with its default configuration.
35+
*
36+
* @see CorePrefixFilter
3637
*/
3738
public CoreTypePool() {
38-
//By default optimise for jakarta annotations, and java util collections
39-
this("jakarta.", "java.", "org.hibernate.annotations.");
39+
this( CorePrefixFilter.DEFAULT_INSTANCE );
4040
}
4141

4242
/**
4343
* Construct a new {@link CoreTypePool} with a choice of which prefixes
4444
* for fully qualified classnames will be loaded by this {@link TypePool}.
45+
*
46+
* @deprecated used by Quarkus
4547
*/
48+
@Deprecated
4649
public CoreTypePool(final String... acceptedPrefixes) {
50+
this( new CorePrefixFilter( acceptedPrefixes ) );
51+
}
52+
53+
public CoreTypePool(CorePrefixFilter acceptedPrefixes) {
4754
//While we implement a cache in this class we also want to enable
4855
//ByteBuddy's default caching mechanism as it will cache the more
4956
//useful output of the parsing and introspection of such types.
50-
super( new TypePool.CacheProvider.Simple() );
51-
this.acceptedPrefixes = acceptedPrefixes;
52-
}
53-
54-
private boolean isCoreClassName(final String name) {
55-
for ( String acceptedPrefix : this.acceptedPrefixes ) {
56-
if ( name.startsWith( acceptedPrefix ) ) {
57-
return true;
58-
}
59-
}
60-
return false;
57+
super( new CoreCacheProvider( acceptedPrefixes ) );
58+
this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes );
6159
}
6260

6361
@Override
6462
protected Resolution doDescribe(final String name) {
65-
if ( isCoreClassName( name ) ) {
63+
if ( acceptedPrefixes.isCoreClassName( name ) ) {
6664
final Resolution resolution = resolutions.get( name );
6765
if ( resolution != null ) {
6866
return resolution;

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ public void discoverTypes(String className, byte[] originalBytes) {
165165
}
166166

167167
private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builderSupplier, TypeDescription managedCtClass) {
168+
// skip if the class was already enhanced. This is very common in WildFly as classloading is highly concurrent.
169+
// We need to ensure that no class is instrumented multiple times as that might result in incorrect bytecode.
170+
// N.B. there is a second check below using a different approach: checking for the marker interfaces,
171+
// which does not address the case of extended bytecode enhancement
172+
// (because it enhances classes that do not end up with these marker interfaces).
173+
// I'm currently inclined to keep both checks, as one is safer and the other has better backwards compatibility.
174+
if ( managedCtClass.getDeclaredAnnotations().isAnnotationPresent( EnhancementInfo.class ) ) {
175+
verifyVersions( managedCtClass, enhancementContext );
176+
log.debugf( "Skipping enhancement of [%s]: it's already annotated with @EnhancementInfo", managedCtClass.getName() );
177+
return null;
178+
}
179+
168180
// can't effectively enhance interfaces
169181
if ( managedCtClass.isInterface() ) {
170182
log.debugf( "Skipping enhancement of [%s]: it's an interface", managedCtClass.getName() );
@@ -181,7 +193,7 @@ private DynamicType.Builder<?> doEnhance(Supplier<DynamicType.Builder<?>> builde
181193
if ( alreadyEnhanced( managedCtClass ) ) {
182194
verifyVersions( managedCtClass, enhancementContext );
183195

184-
log.debugf( "Skipping enhancement of [%s]: already enhanced", managedCtClass.getName() );
196+
log.debugf( "Skipping enhancement of [%s]: it's already implementing 'Managed'", managedCtClass.getName() );
185197
return null;
186198
}
187199

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ public class ModelTypePool extends TypePool.Default implements EnhancerClassLoca
2020

2121
private final ConcurrentHashMap<String, Resolution> resolutions = new ConcurrentHashMap<>();
2222
private final OverridingClassFileLocator locator;
23+
private final SafeCacheProvider poolCache;
2324

24-
private ModelTypePool(CacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) {
25+
private ModelTypePool(SafeCacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) {
2526
super( cacheProvider, classFileLocator, ReaderMode.FAST, parent );
27+
this.poolCache = cacheProvider;
2628
this.locator = classFileLocator;
2729
}
2830

@@ -62,7 +64,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile
6264
* @return
6365
*/
6466
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) {
65-
return buildModelTypePool( classFileLocator, coreTypePool, new TypePool.CacheProvider.Simple() );
67+
return buildModelTypePool( classFileLocator, coreTypePool, new SafeCacheProvider() );
6668
}
6769

6870
/**
@@ -72,7 +74,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile
7274
* @param cacheProvider
7375
* @return
7476
*/
75-
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, CacheProvider cacheProvider) {
77+
public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, SafeCacheProvider cacheProvider) {
7678
Objects.requireNonNull( classFileLocator );
7779
Objects.requireNonNull( coreTypePool );
7880
Objects.requireNonNull( cacheProvider );
@@ -92,6 +94,13 @@ protected Resolution doDescribe(final String name) {
9294

9395
@Override
9496
public void registerClassNameAndBytes(final String className, final byte[] bytes) {
97+
//Very important: ensure the registered override is actually effective in case this class
98+
//was already resolved in the recent past; this could have happened for example as a side effect
99+
//of symbol resolution during enhancement of a different class, or very simply when attempting
100+
//to re-enhanced the same class - which happens frequently in WildFly because of the class transformers
101+
//being triggered concurrently by multiple parallel deployments.
102+
resolutions.remove( className );
103+
poolCache.remove( className );
95104
locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) );
96105
}
97106

hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,42 @@
77
package org.hibernate.bytecode.enhance.internal.bytebuddy;
88

99
import java.io.IOException;
10+
import java.util.HashMap;
1011
import java.util.Objects;
11-
import java.util.concurrent.ConcurrentHashMap;
1212

1313
import net.bytebuddy.dynamic.ClassFileLocator;
1414

1515
/**
1616
* Allows wrapping another ClassFileLocator to add the ability to define
1717
* resolution overrides for specific resources.
18+
* This is useful when enhancing entities and we need to process an
19+
* input byte array representing the bytecode of the entity, and some
20+
* external party is providing the byte array explicitly, avoiding for
21+
* us having to load it from the classloader.
22+
* We'll still need to load several other symbols from a parent @{@link ClassFileLocator}
23+
* (typically based on the classloader), but need to return the provided
24+
* byte array for some selected resources.
25+
* Any override is scoped to the current thread; this is to avoid
26+
* interference among threads in systems which perform parallel
27+
* class enhancement, for example containers requiring "on the fly"
28+
* transformation of classes as they are being loaded will often
29+
* perform such operations concurrently.
1830
*/
19-
public final class OverridingClassFileLocator implements ClassFileLocator {
31+
final class OverridingClassFileLocator implements ClassFileLocator {
2032

21-
private final ConcurrentHashMap<String, Resolution> registeredResolutions = new ConcurrentHashMap<>();
33+
private final ThreadLocal<HashMap<String, Resolution>> registeredResolutions = ThreadLocal.withInitial( () -> new HashMap<>() );
2234
private final ClassFileLocator parent;
2335

24-
public OverridingClassFileLocator(final ClassFileLocator parent) {
36+
/**
37+
* @param parent the @{@link ClassFileLocator} which will be used to load any resource which wasn't explicitly registered as an override.
38+
*/
39+
OverridingClassFileLocator(final ClassFileLocator parent) {
2540
this.parent = Objects.requireNonNull( parent );
2641
}
2742

2843
@Override
2944
public Resolution locate(final String name) throws IOException {
30-
final Resolution resolution = registeredResolutions.get( name );
45+
final Resolution resolution = getLocalMap().get( name );
3146
if ( resolution != null ) {
3247
return resolution;
3348
}
@@ -36,17 +51,32 @@ public Resolution locate(final String name) throws IOException {
3651
}
3752
}
3853

54+
private HashMap<String, Resolution> getLocalMap() {
55+
return registeredResolutions.get();
56+
}
57+
3958
@Override
40-
public void close() throws IOException {
41-
//Nothing to do: we're not responsible for parent
59+
public void close() {
60+
registeredResolutions.remove();
4261
}
4362

63+
/**
64+
* Registers an explicit resolution override
65+
*
66+
* @param className
67+
* @param explicit
68+
*/
4469
void put(String className, Resolution.Explicit explicit) {
45-
registeredResolutions.put( className, explicit );
70+
getLocalMap().put( className, explicit );
4671
}
4772

73+
/**
74+
* Removes an explicit resolution override
75+
*
76+
* @param className
77+
*/
4878
void remove(String className) {
49-
registeredResolutions.remove( className );
79+
getLocalMap().remove( className );
5080
}
5181

5282
}

0 commit comments

Comments
 (0)