Skip to content

Commit 6f4eda7

Browse files
authored
Merge pull request #50 from jbagdis/bugfix/myclassloader-idempotency
Fix MyClassLoader idempotency - issue #49
2 parents 2cb91b0 + a609ce1 commit 6f4eda7

File tree

3 files changed

+364
-38
lines changed

3 files changed

+364
-38
lines changed

afterburner/src/main/java/com/fasterxml/jackson/module/afterburner/util/MyClassLoader.java

Lines changed: 150 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import java.lang.reflect.Method;
44
import java.nio.charset.Charset;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.logging.Level;
7+
import java.util.logging.Logger;
58

69
/**
710
* Class loader that is needed to load generated classes.
@@ -10,6 +13,11 @@ public class MyClassLoader extends ClassLoader
1013
{
1114
private final static Charset UTF8 = Charset.forName("UTF-8");
1215

16+
// Maps parent classloader instance and class name to the corresponding lock object.
17+
// N.B. this must be static because multiple instances of MyClassLoader must all use the same lock
18+
// when loading classes directly on the same parent.
19+
private final static ConcurrentHashMap<String, Object> parentParallelLockMap = new ConcurrentHashMap<>();
20+
1321
/**
1422
* Flag that determines if we should first try to load new class
1523
* using parent class loader or not; this may be done to try to
@@ -56,52 +64,156 @@ public static boolean canAddClassInPackageOf(Class<?> cls)
5664
* implement
5765
*/
5866
public Class<?> loadAndResolve(ClassName className, byte[] byteCode)
59-
throws IllegalArgumentException
67+
throws IllegalArgumentException
6068
{
61-
// First things first: just to be sure; maybe we have already loaded it?
62-
Class<?> old = findLoadedClass(className.getDottedName());
63-
if (old != null) {
64-
return old;
69+
// first, try to loadAndResolve via the parent classloader, if configured to do so
70+
Class<?> classFromParent = loadAndResolveUsingParentClassloader(className, byteCode);
71+
if (classFromParent != null) {
72+
return classFromParent;
6573
}
66-
67-
Class<?> impl;
68-
69-
// Important: bytecode is generated with a template name (since bytecode itself
70-
// is used for checksum calculation) -- must be replaced now, however
71-
replaceName(byteCode, className.getSlashedTemplate(), className.getSlashedName());
72-
73-
// First: let's try calling it directly on parent, to be able to access protected/package-access stuff:
74-
if (_cfgUseParentLoader) {
75-
ClassLoader cl = getParent();
76-
// if we have parent, that is
77-
if (cl != null) {
78-
try {
79-
Method method = ClassLoader.class.getDeclaredMethod("defineClass",
80-
new Class[] {String.class, byte[].class, int.class,
81-
int.class});
82-
method.setAccessible(true);
83-
return (Class<?>)method.invoke(getParent(),
84-
className.getDottedName(), byteCode, 0, byteCode.length);
85-
} catch (Exception e) {
86-
// Should we handle this somehow?
74+
75+
// fall back to loading and resolving ourselves
76+
synchronized (getClassLoadingLock(className.getDottedName())) {
77+
// First: check to see if we have loaded it ourselves
78+
Class<?> existingClass = findLoadedClass(className.getDottedName());
79+
if (existingClass != null) {
80+
return existingClass;
81+
}
82+
83+
// Important: bytecode is generated with a template name (since bytecode itself
84+
// is used for checksum calculation) -- must be replaced now, however
85+
replaceName(byteCode, className.getSlashedTemplate(), className.getSlashedName());
86+
87+
// Second: define a new class instance using the bytecode
88+
Class<?> newClass;
89+
try {
90+
newClass = defineClass(className.getDottedName(), byteCode, 0, byteCode.length);
91+
} catch (LinkageError e) {
92+
Throwable t = e;
93+
while (t.getCause() != null) {
94+
t = t.getCause();
8795
}
96+
throw new IllegalArgumentException("Failed to load class '" + className + "': " + t.getMessage(), t);
8897
}
98+
// important: must also resolve the newly-created class.
99+
resolveClass(newClass);
100+
return newClass;
89101
}
102+
}
90103

91-
// but if that doesn't fly, try to do it from our own class loader
92-
93-
try {
94-
impl = defineClass(className.getDottedName(), byteCode, 0, byteCode.length);
95-
} catch (LinkageError e) {
96-
Throwable t = e;
97-
while (t.getCause() != null) {
98-
t = t.getCause();
104+
/**
105+
* Attempt to load (and resolve) the class using the parent class loader (if it is configured and present).
106+
* This method will return {@code null} if the parent classloader is not configured or cannot be retrieved.
107+
*
108+
* @param className Interface or abstract class that class to load should extend or implement
109+
* @param byteCode the generated bytecode for the class to load
110+
* @return the loaded class, or {@code null} if the class could not be loaded on the parent classloader.
111+
*/
112+
private Class<?> loadAndResolveUsingParentClassloader(ClassName className, byte[] byteCode)
113+
{
114+
ClassLoader parentClassLoader;
115+
if (!_cfgUseParentLoader || (parentClassLoader = getParent()) == null) {
116+
return null;
117+
}
118+
// N.B. The parent-class-loading locks are shared between all instances of MyClassLoader.
119+
// We can be confident that no attempt will be made to re-acquire *any* parent-class-loading lock instance
120+
// inside the synchronized region (eliminating the risk of deadlock), even if the parent class loader is also
121+
// an instance of MyClassLoader, because:
122+
// a) this method is the only place that attempts to acquire a parent class loading lock,
123+
// b) the only non-private method which calls this method and thus acquires this lock is
124+
// MyClassLoader#loadAndResolve,
125+
// c) nothing in the synchronized region can have the effect of calling #loadAndResolve on this
126+
// or any other instance of MyClassLoader.
127+
synchronized (getParentClassLoadingLock(parentClassLoader, className.getDottedName())) {
128+
// First: check to see if the parent classloader has loaded it already
129+
Class<?> impl = findLoadedClassOnParent(parentClassLoader, className.getDottedName());
130+
if (impl != null) {
131+
return impl;
99132
}
100-
throw new IllegalArgumentException("Failed to load class '"+className+"': "+t.getMessage(), t);
133+
134+
// Important: bytecode is generated with a template name (since bytecode itself
135+
// is used for checksum calculation) -- must be replaced now, however
136+
replaceName(byteCode, className.getSlashedTemplate(), className.getSlashedName());
137+
138+
// Second: define a new class instance on the parent classloder using the bytecode
139+
impl = defineClassOnParent(parentClassLoader, className.getDottedName(), byteCode, 0, byteCode.length);
140+
// important: must also resolve the newly-created class.
141+
resolveClassOnParent(parentClassLoader, impl);
142+
return impl;
143+
}
144+
}
145+
146+
/**
147+
* Get the class loading lock for the parent class loader for loading the named class.
148+
*
149+
* This is effectively the same implementation as ClassLoader#getClassLoadingLock, but using
150+
* our static parentParallelLockMap and keying off of the parent ClassLoader instance as well as
151+
* the class name to load.
152+
*
153+
* @param parentClassLoader The parent ClassLoader
154+
* @param className The name of the to-be-loaded class
155+
*/
156+
private Object getParentClassLoadingLock(ClassLoader parentClassLoader, String className) {
157+
// N.B. using the canonical name and identity hash code to represent the parent class loader in the key
158+
// in case that ClassLoader instance (which could be anything) implements #hashCode or #toString poorly.
159+
// In the event of a collision here (same key, different parent class loader), we will end up using the
160+
// same lock only to synchronize loads of the same class on two different class loaders,
161+
// which shouldn't ever deadlock (see proof in #loadAndResolveUsingParentClassloader);
162+
// worst case is unnecessary contention for the lock.
163+
String key = parentClassLoader.getClass().getCanonicalName()
164+
+ ":" + System.identityHashCode(parentClassLoader)
165+
+ ":" + className;
166+
Object newLock = new Object();
167+
Object lock = parentParallelLockMap.putIfAbsent(key, newLock);
168+
if (lock == null) {
169+
lock = newLock;
170+
}
171+
return lock;
172+
}
173+
174+
private Class<?> findLoadedClassOnParent(ClassLoader parentClassLoader, String className) {
175+
try {
176+
Method method = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
177+
method.setAccessible(true);
178+
return (Class<?>) method.invoke(parentClassLoader, className);
179+
} catch (Exception e) {
180+
String msg = String.format("Exception trying 'findLoadedClass(%s)' on parent ClassLoader '%s'",
181+
className, parentClassLoader);
182+
Logger.getLogger(MyClassLoader.class.getName()).log(Level.FINE, msg, e);
183+
return null;
184+
}
185+
}
186+
187+
// visible for testing
188+
Class<?> defineClassOnParent(ClassLoader parentClassLoader,
189+
String className,
190+
byte[] byteCode,
191+
int offset,
192+
int length) {
193+
try {
194+
Method method = ClassLoader.class.getDeclaredMethod("defineClass",
195+
new Class[]{String.class, byte[].class, int.class, int.class});
196+
method.setAccessible(true);
197+
return (Class<?>) method.invoke(parentClassLoader,
198+
className, byteCode, offset, length);
199+
} catch (Exception e) {
200+
String msg = String.format("Exception trying 'defineClass(%s, <bytecode>)' on parent ClassLoader '%s'",
201+
className, parentClassLoader);
202+
Logger.getLogger(MyClassLoader.class.getName()).log(Level.FINE, msg, e);
203+
return null;
204+
}
205+
}
206+
207+
private void resolveClassOnParent(ClassLoader parentClassLoader, Class<?> clazz) {
208+
try {
209+
Method method = ClassLoader.class.getDeclaredMethod("resolveClass", Class.class);
210+
method.setAccessible(true);
211+
method.invoke(parentClassLoader, clazz);
212+
} catch (Exception e) {
213+
String msg = String.format("Exception trying 'resolveClass(%s)' on parent ClassLoader '%s'",
214+
clazz, parentClassLoader);
215+
Logger.getLogger(MyClassLoader.class.getName()).log(Level.FINE, msg, e);
101216
}
102-
// important: must also resolve the class...
103-
resolveClass(impl);
104-
return impl;
105217
}
106218

107219
public static int replaceName(byte[] byteCode,

0 commit comments

Comments
 (0)