@@ -18,6 +18,70 @@ public class MyClassLoader extends ClassLoader
1818 // when loading classes directly on the same parent.
1919 private final static ConcurrentHashMap <String , Object > parentParallelLockMap = new ConcurrentHashMap <>();
2020
21+ /**
22+ * True if {@link ClassLoader#findLoadedClass(String)} and
23+ * {@link ClassLoader#defineClass(String, byte[], int, int)} are reflectively
24+ * accessible from this class. On JDK 9+ both methods live in a non-open
25+ * package of {@code java.base}, so reflective access requires the JVM to be
26+ * launched with {@code --add-opens java.base/java.lang=ALL-UNNAMED}. Without
27+ * that flag, {@link #loadAndResolveUsingParentClassloader} cannot cache
28+ * generated classes on the bean's parent classloader and each call to
29+ * {@link #loadAndResolve} ends up defining the class in a throwaway
30+ * {@link MyClassLoader} instance — correctness is preserved but class
31+ * metaspace grows with mapper count. See
32+ * <a href="https://github.com/FasterXML/jackson-modules-base/issues/348">issue #348</a>.
33+ *
34+ * Checked once in a static initializer; cached here for the rest of this
35+ * class's lifetime. When {@code false}, {@link #loadAndResolveUsingParentClassloader}
36+ * short-circuits immediately rather than paying the per-call
37+ * try/catch + logging cost.
38+ *
39+ * @since 3.2
40+ */
41+ private static final boolean PARENT_CL_REFLECTION_AVAILABLE ;
42+ static {
43+ boolean ok = false ;
44+ try {
45+ // Probe both methods we'll later try to invoke. Either one failing
46+ // is enough to disable the parent-classloader cache path.
47+ Method find = ClassLoader .class .getDeclaredMethod ("findLoadedClass" , String .class );
48+ find .setAccessible (true );
49+ Method define = ClassLoader .class .getDeclaredMethod ("defineClass" ,
50+ String .class , byte [].class , int .class , int .class );
51+ define .setAccessible (true );
52+ ok = true ;
53+ } catch (Throwable t ) {
54+ // Swallow; ok stays false. We log once below at WARNING level so
55+ // users running on JDK 9+ without the required --add-opens flag
56+ // see a clear, actionable message instead of silent degradation.
57+ Logger .getLogger (MyClassLoader .class .getName ()).log (Level .WARNING ,
58+ "Afterburner: unable to reflectively access ClassLoader#findLoadedClass"
59+ + " / ClassLoader#defineClass on the parent class loader."
60+ + " On JDK 9+ this requires launching the JVM with"
61+ + " `--add-opens java.base/java.lang=ALL-UNNAMED`."
62+ + " Without it, Afterburner cannot cache generated mutator/accessor"
63+ + " classes on the bean's classloader, so each new ObjectMapper"
64+ + " generates fresh classes per POJO — functional behavior is"
65+ + " preserved but classloader count grows with mapper count."
66+ + " See https://github.com/FasterXML/jackson-modules-base/issues/348"
67+ + " for context. Reason: " + t );
68+ }
69+ PARENT_CL_REFLECTION_AVAILABLE = ok ;
70+ }
71+
72+ /**
73+ * Returns {@code true} iff Afterburner can use the parent class loader to
74+ * cache generated mutator / accessor classes across {@code ObjectMapper}
75+ * instances. See {@link #PARENT_CL_REFLECTION_AVAILABLE} for details and
76+ * <a href="https://github.com/FasterXML/jackson-modules-base/issues/348">issue #348</a>
77+ * for the user-facing symptom when this returns {@code false}.
78+ *
79+ * @since 3.2
80+ */
81+ public static boolean isParentClassLoaderReflectionAvailable () {
82+ return PARENT_CL_REFLECTION_AVAILABLE ;
83+ }
84+
2185 /**
2286 * Flag that determines if we should first try to load new class
2387 * using parent class loader or not; this may be done to try to
@@ -121,6 +185,12 @@ public Class<?> loadAndResolve(ClassName className, byte[] byteCode)
121185 */
122186 private Class <?> loadAndResolveUsingParentClassloader (ClassName className , byte [] byteCode )
123187 {
188+ // Short-circuit when we already know the reflective path into ClassLoader
189+ // isn't available (e.g. running on JDK 9+ without --add-opens java.base/java.lang).
190+ // Avoids per-call try/catch overhead and keeps FINE-level log noise down.
191+ if (!PARENT_CL_REFLECTION_AVAILABLE ) {
192+ return null ;
193+ }
124194 ClassLoader parentClassLoader ;
125195 if (!_cfgUseParentLoader || (parentClassLoader = getParent ()) == null ) {
126196 return null ;
0 commit comments