Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package tools.jackson.module.afterburner.inject;

import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

// Verifies the detection hook for issue #348: afterburner now probes at static
// init time whether ClassLoader#findLoadedClass / ClassLoader#defineClass are
// reflectively accessible. The result is exposed via a static method
// MyClassLoader.isParentClassLoaderReflectionAvailable(). If the probe fails,
// afterburner logs a WARNING once and short-circuits the parent-classloader
// cache path.
//
// This test has two jobs:
//
// 1. Confirm the public accessor exists and is reachable — i.e. a future
// refactor can't silently remove it without breaking this test.
//
// 2. Confirm it returns `true` in this test environment. The afterburner-tests
// pom passes `--add-opens java.base/java.lang=ALL-UNNAMED` on the surefire
// argLine precisely so the probe succeeds. If this assertion ever fails,
// either the argLine was dropped (fix the pom) or afterburner's probe logic
// broke (fix the probe). Either way, the GeneratedClassCachingTest's
// `testSameBeanAcrossMappersReusesSameMutatorClass` assertion will also
// fail — but this test gives a more direct failure message pointing at
// the probe, not the consequence.
public class ClassLoaderReflectionProbeTest
{
private static final String MY_CL =
"tools.jackson.module.afterburner.util.MyClassLoader";

@Test
public void testProbeAccessorExistsAndIsPublic() throws Exception
{
// MyClassLoader is in a non-exported package of the afterburner JPMS
// module, but we're in the unnamed module (classpath), so we can
// reflectively reach it. `isParentClassLoaderReflectionAvailable` is
// a public static method so the probe result is observable without
// poking at private fields.
Class<?> myClassLoader = Class.forName(MY_CL);
Method m = myClassLoader.getMethod("isParentClassLoaderReflectionAvailable");
assertTrue(java.lang.reflect.Modifier.isPublic(m.getModifiers()),
"isParentClassLoaderReflectionAvailable() should be public");
assertTrue(java.lang.reflect.Modifier.isStatic(m.getModifiers()),
"isParentClassLoaderReflectionAvailable() should be static");
assertEquals(boolean.class, m.getReturnType(),
"isParentClassLoaderReflectionAvailable() should return boolean");
}

@Test
public void testProbeReturnsTrueInTestEnvironment() throws Exception
{
// The afterburner-tests pom sets
// `--add-opens java.base/java.lang=ALL-UNNAMED` on the surefire
// argLine (see the pom for rationale + issue #348). With that flag
// in place, the probe must succeed — otherwise either the pom was
// tampered with, or afterburner's probe logic is broken.
Class<?> myClassLoader = Class.forName(MY_CL);
Method m = myClassLoader.getMethod("isParentClassLoaderReflectionAvailable");
Object result = m.invoke(null);
assertEquals(Boolean.TRUE, result,
"Expected parent-classloader reflection to be available in"
+ " the afterburner-tests environment, because this"
+ " module's surefire argLine sets"
+ " `--add-opens java.base/java.lang=ALL-UNNAMED`. If"
+ " the probe is returning false, either the argLine"
+ " was dropped from the pom or the probe logic in"
+ " MyClassLoader's static initializer is broken."
+ " See issue #348 for context.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,70 @@ public class MyClassLoader extends ClassLoader
// when loading classes directly on the same parent.
private final static ConcurrentHashMap<String, Object> parentParallelLockMap = new ConcurrentHashMap<>();

/**
* True if {@link ClassLoader#findLoadedClass(String)} and
* {@link ClassLoader#defineClass(String, byte[], int, int)} are reflectively
* accessible from this class. On JDK 9+ both methods live in a non-open
* package of {@code java.base}, so reflective access requires the JVM to be
* launched with {@code --add-opens java.base/java.lang=ALL-UNNAMED}. Without
* that flag, {@link #loadAndResolveUsingParentClassloader} cannot cache
* generated classes on the bean's parent classloader and each call to
* {@link #loadAndResolve} ends up defining the class in a throwaway
* {@link MyClassLoader} instance — correctness is preserved but class
* metaspace grows with mapper count. See
* <a href="https://github.com/FasterXML/jackson-modules-base/issues/348">issue #348</a>.
*
* Checked once in a static initializer; cached here for the rest of this
* class's lifetime. When {@code false}, {@link #loadAndResolveUsingParentClassloader}
* short-circuits immediately rather than paying the per-call
* try/catch + logging cost.
*
* @since 3.2
*/
private static final boolean PARENT_CL_REFLECTION_AVAILABLE;
static {
boolean ok = false;
try {
// Probe both methods we'll later try to invoke. Either one failing
// is enough to disable the parent-classloader cache path.
Method find = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
find.setAccessible(true);
Method define = ClassLoader.class.getDeclaredMethod("defineClass",
String.class, byte[].class, int.class, int.class);
define.setAccessible(true);
ok = true;
} catch (Throwable t) {
// Swallow; ok stays false. We log once below at WARNING level so
// users running on JDK 9+ without the required --add-opens flag
// see a clear, actionable message instead of silent degradation.
Logger.getLogger(MyClassLoader.class.getName()).log(Level.WARNING,
"Afterburner: unable to reflectively access ClassLoader#findLoadedClass"
+ " / ClassLoader#defineClass on the parent class loader."
+ " On JDK 9+ this requires launching the JVM with"
+ " `--add-opens java.base/java.lang=ALL-UNNAMED`."
+ " Without it, Afterburner cannot cache generated mutator/accessor"
+ " classes on the bean's classloader, so each new ObjectMapper"
+ " generates fresh classes per POJO — functional behavior is"
+ " preserved but classloader count grows with mapper count."
+ " See https://github.com/FasterXML/jackson-modules-base/issues/348"
+ " for context. Reason: " + t);
}
PARENT_CL_REFLECTION_AVAILABLE = ok;
}

/**
* Returns {@code true} iff Afterburner can use the parent class loader to
* cache generated mutator / accessor classes across {@code ObjectMapper}
* instances. See {@link #PARENT_CL_REFLECTION_AVAILABLE} for details and
* <a href="https://github.com/FasterXML/jackson-modules-base/issues/348">issue #348</a>
* for the user-facing symptom when this returns {@code false}.
*
* @since 3.2
*/
public static boolean isParentClassLoaderReflectionAvailable() {
return PARENT_CL_REFLECTION_AVAILABLE;
}

/**
* Flag that determines if we should first try to load new class
* using parent class loader or not; this may be done to try to
Expand Down Expand Up @@ -121,6 +185,12 @@ public Class<?> loadAndResolve(ClassName className, byte[] byteCode)
*/
private Class<?> loadAndResolveUsingParentClassloader(ClassName className, byte[] byteCode)
{
// Short-circuit when we already know the reflective path into ClassLoader
// isn't available (e.g. running on JDK 9+ without --add-opens java.base/java.lang).
// Avoids per-call try/catch overhead and keeps FINE-level log noise down.
if (!PARENT_CL_REFLECTION_AVAILABLE) {
return null;
}
ClassLoader parentClassLoader;
if (!_cfgUseParentLoader || (parentClassLoader = getParent()) == null) {
return null;
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ Active maintainers:
#347: (afterburner) Added module "afterburner-tests" for better Afterburner-in-classpath
testing
(fix by @cowtowncoder, w/ Claude code)
#348 (afterburner) Afterburner parent-classloader caching silently broken on
Java 9+ without `--add-opens java.base/java.lang=ALL-UNNAMED`
(fix by @cowtowncoder, w/ Claude code)
#349 (blackbird) Added module "blackbird-tests" for classpath-mode Blackbird coverage
(setter specializations, field-access limitation, CrossLoaderAccess fast-path)
(fix by @cowtowncoder, w/ Claude code)
Expand Down