|
| 1 | +# jackson-module-afterburner-tests |
| 2 | + |
| 3 | +Classpath-mode integration tests for `jackson-module-afterburner`. **This module is |
| 4 | +test-only and is never published.** |
| 5 | + |
| 6 | +## Why this module exists |
| 7 | + |
| 8 | +Afterburner's most valuable optimizations come from bytecode injection: |
| 9 | +`PropertyMutatorCollector`, `PropertyAccessorCollector`, and `CreatorOptimizer` |
| 10 | +generate custom mutator / accessor / instantiator classes in the *target bean's |
| 11 | +package* and install them on the bean's `ClassLoader`. This only works when the |
| 12 | +target package is not sealed. |
| 13 | + |
| 14 | +In Jackson 3.x every module descriptor — including afterburner's own test module |
| 15 | +— is a named JPMS module, and the JVM automatically seals every package inside a |
| 16 | +named module. As a result, every POJO defined inside `tools.jackson.module.afterburner` |
| 17 | +test sources fails the `MyClassLoader.canAddClassInPackageOf` check, and afterburner |
| 18 | +silently falls back to plain reflection. That means **none** of afterburner's |
| 19 | +injection paths are exercised by the in-tree afterburner test suite — the optimized |
| 20 | +code path is effectively dead weight in CI even though the tests appear to pass. |
| 21 | + |
| 22 | +This module closes that gap. It lives alongside the afterburner module but |
| 23 | +deliberately: |
| 24 | + |
| 25 | +- has **no `module-info.java`** in either main or test sources, |
| 26 | +- is configured (`useModulePath=false`) to run its tests on the classpath, |
| 27 | +- and therefore loads its test POJOs into the **unnamed module**, whose packages |
| 28 | + are not sealed. |
| 29 | + |
| 30 | +Afterburner's injection pipeline runs on those POJOs normally, and the tests here |
| 31 | +verify via reflection on databind internals that each property was actually |
| 32 | +replaced with an `OptimizedSettableBeanProperty` / `OptimizedBeanPropertyWriter` |
| 33 | +and each default-creator POJO got an `OptimizedValueInstantiator`. |
| 34 | + |
| 35 | +## Do not add a `module-info.java` here |
| 36 | + |
| 37 | +Adding a module descriptor would put these test POJOs back into a named module, |
| 38 | +re-seal their packages, and silently turn this whole module into dead weight — |
| 39 | +same as the in-tree afterburner tests. |
| 40 | + |
| 41 | +If a future cleanup pass wants to "unify" the project layout by making every |
| 42 | +module JPMS-consistent, this one is the exception. Read this file before doing |
| 43 | +that. |
| 44 | + |
| 45 | +## Do not publish this module |
| 46 | + |
| 47 | +Several parent-POM plugin bindings are unbound or skipped in `pom.xml` so that |
| 48 | +`mvn install` on this module produces no jar, no SBOM, no Gradle module metadata, |
| 49 | +and no OSGi bundle. The module exists purely to run tests. If you rename an |
| 50 | +execution override in `pom.xml`, re-verify with: |
| 51 | + |
| 52 | +``` |
| 53 | +./mvnw help:effective-pom -pl afterburner-tests |
| 54 | +``` |
| 55 | + |
| 56 | +A typo in an execution id turns into silent dead config and leaves stale |
| 57 | +artifacts in `target/` rather than a build error. |
| 58 | + |
| 59 | +## What is covered |
| 60 | + |
| 61 | +- `PropertyMutatorCollector` — int, long, boolean, and reference-type |
| 62 | + specializations for both public-field and setter-method access |
| 63 | + (`MutatorSpecializationsTest`). |
| 64 | +- `PropertyAccessorCollector` — writer replacement on the serializer side |
| 65 | + (`SerializerInjectionTest`). |
| 66 | +- `CreatorOptimizer` — default-constructor and static-factory positive cases, |
| 67 | + plus property-based `@JsonCreator` negative case (`CreatorOptimizerTest`). |
| 68 | +- `MyClassLoader.defineClass` — transitively exercised by all of the above. |
| 69 | + |
| 70 | +## What is *not* covered |
| 71 | + |
| 72 | +- Private-class guard (`_classLoader != null && isPrivate(beanClass)`). |
| 73 | +- `setUseValueClassLoader(false)` toggle. |
| 74 | +- Concurrent deserializer construction. |
| 75 | +- Fallback path when afterburner correctly refuses to optimize (e.g. a bean |
| 76 | + whose package really is sealed — the in-tree afterburner tests inadvertently |
| 77 | + cover this). |
| 78 | +- GraalVM native-image disable path in `AfterburnerModule.setupModule`. |
| 79 | + |
| 80 | +## Known findings from this module |
| 81 | + |
| 82 | +### Afterburner parent-classloader caching silently broken on Java 9+ (#348) |
| 83 | + |
| 84 | +While writing `GeneratedClassCachingTest`, we discovered that Afterburner's |
| 85 | +parent-classloader cache — the mechanism that's supposed to make two |
| 86 | +independent `ObjectMapper` instances share a single generated mutator class |
| 87 | +per POJO — does not work on Java 9+ unless the JVM is launched with |
| 88 | +`--add-opens java.base/java.lang=ALL-UNNAMED`. `MyClassLoader` reflects into |
| 89 | +`ClassLoader#findLoadedClass` and `ClassLoader#defineClass`, both protected |
| 90 | +members of `java.lang.ClassLoader`; on modern JDKs the reflective access |
| 91 | +throws `InaccessibleObjectException`, which Afterburner catches silently and |
| 92 | +falls back to defining each generated class in a fresh throwaway loader. |
| 93 | + |
| 94 | +That's why this module's `pom.xml` sets |
| 95 | +`--add-opens java.base/java.lang=ALL-UNNAMED` on the surefire `argLine`. |
| 96 | +Without it, `testSameBeanAcrossMappersReusesSameMutatorClass` fails with two |
| 97 | +distinct `Class<?>` instances carrying identical fully-qualified names — the |
| 98 | +exact fingerprint of the leak. This is **not a test-environment quirk**; it |
| 99 | +affects every real Afterburner user on Java 9+. See issue #348 for the fix |
| 100 | +plan. |
| 101 | + |
| 102 | +## References |
| 103 | + |
| 104 | +- PR #347: rationale for the module's structure and initial coverage |
| 105 | +- Issue #348: afterburner classloader caching regression (surfaced here) |
0 commit comments