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
97 changes: 97 additions & 0 deletions blackbird-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# jackson-module-blackbird-tests

Classpath-mode integration tests for `jackson-module-blackbird`. **This module is
test-only and is never published.**

## Why this module exists

This module is the Blackbird counterpart to
[`afterburner-tests`](../afterburner-tests/README.md). The coverage gap it
addresses is narrower than Afterburner's, because Blackbird's optimizer
mechanism is structurally different — and better aligned with JPMS — than
Afterburner's.

**Afterburner:** uses ByteBuddy + `ClassLoader.defineClass` + reflective
access to `java.lang.ClassLoader` methods. On Java 9+, reflective access to
`ClassLoader` is gated by JPMS, and in the JPMS-named test module every test
POJO ends up in a sealed package. That combination silently disables the
optimizer in the in-tree afterburner test suite. See the `afterburner-tests`
README for the full story.

**Blackbird:** uses `MethodHandles.Lookup.defineClass` (a JDK 9+ supported
API) + `MethodHandles.privateLookupIn`. Neither needs reflective access to
`ClassLoader` methods, neither cares whether the target package is "sealed by
module", and `privateLookupIn` works across the JPMS module boundary whenever
the target module opens its package to the caller (or lives in the unnamed
module). As a result, Blackbird's in-tree tests **do** exercise the optimizer
for setter-based POJOs.

What's NOT exercised by Blackbird's in-tree tests, and is covered here:

- **Classpath / unnamed-module POJOs.** A real-world POJO loaded by the
system class loader rather than as part of the Blackbird JPMS module.
This module's test POJOs live in the unnamed module (no `module-info.java`
anywhere), so the optimizer has to cross the module boundary.
- **The documented limitations of Blackbird's optimizer** — specifically
that direct public-field access is **not** optimized on either the
deserializer or serializer side. Blackbird's
`BBDeserializerModifier.nextProperty` and
`BBSerializerModifier.createProperty` both skip non-method members.
- **The `CrossLoaderAccess` fast-path behavior.** Blackbird's
`CrossLoaderAccess` contains a slow-path that defines a
`$$JacksonBlackbirdAccess` companion class via `Lookup.defineClass(byte[])`
to upgrade a partial-privilege lookup. `CrossLoaderAccessTest` pins the
current behavior that on JDK 9+ with an unnamed-module bean, the fast
path always wins and the companion class is never defined.

## Do not add a `module-info.java` here

Same reason as `afterburner-tests`: adding a module descriptor would put the
test POJOs back into a named module and change how `privateLookupIn`
resolves them, silently undermining the classpath-coverage purpose.

## Do not publish this module

Several parent-POM plugin bindings are unbound or skipped in `pom.xml` so
that `mvn install` on this module produces no jar, no SBOM, no Gradle module
metadata, and no OSGi bundle. See `afterburner-tests/pom.xml` for the same
pattern with extended commentary; this module's `pom.xml` shares the same
shape.

## What is covered

- `BBDeserializerModifier` — setter specializations (int / long / boolean /
String / Object) in `SetterOptimizationTest`.
- `BBSerializerModifier` — getter-based writer specializations in
`SerializerInjectionTest`.
- Field-access negative cases (known design limitation) on both sides, in
`FieldAccessNotOptimizedTest` and `SerializerInjectionTest`.
- `CrossLoaderAccess` fast-path short-circuit for unnamed-module beans, in
`CrossLoaderAccessTest`.

## What is *not* covered

- Private-class guard (`Modifier.isPrivate(beanClass)` in both modifiers).
- The slow-path `CrossLoaderAccess.accessClassIn` companion-class definition
— that code is effectively dead for classpath POJOs on JDK 9+; see
`CrossLoaderAccessTest` javadoc for the explanation.
- `CreatorOptimizer` — Blackbird has one; a follow-up could mirror
`afterburner-tests/CreatorOptimizerTest`.
- Concurrent deserializer construction.
- Bean classes in a JPMS module other than Blackbird's own. Testing that
would require a third submodule with its own `module-info.java` that
`opens` its package to Blackbird, which adds complexity for unclear
additional value.
- Lambda-classloader growth as a function of mapper count. Blackbird's
fundamental design creates a new anonymous lambda class per
`LambdaMetafactory.metafactory` invocation; this isn't a bug and isn't
caching-related, so there's nothing to assert.

## References

- PR that introduced this module — see git history
- [`afterburner-tests/README.md`](../afterburner-tests/README.md) for the
sibling module and the broader rationale
- Issue #348 — analogous (but different) afterburner classloader caching
issue. Does not apply to Blackbird: Blackbird uses `Lookup.defineClass`,
not reflective `ClassLoader.defineClass`, so it is structurally immune.
123 changes: 123 additions & 0 deletions blackbird-tests/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tools.jackson.module</groupId>
<artifactId>jackson-modules-base</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>

<artifactId>jackson-module-blackbird-tests</artifactId>
<name>Jackson module: Blackbird integration tests</name>
<packaging>jar</packaging>

<description>Classpath-mode integration tests for jackson-module-blackbird. Unlike
jackson-module-afterburner-tests, Blackbird's in-tree tests already exercise most of
the optimizer because Blackbird uses the JDK 9+ supported MethodHandles.Lookup API
rather than reflective ClassLoader access. What this module adds is coverage of the
classpath/unnamed-module path: when a POJO lives outside Blackbird's own JPMS module,
Blackbird's CrossLoaderAccess has to define a $$JacksonBlackbirdAccess companion class
via Lookup.defineClass to upgrade the caller lookup to one with full privilege access
over the target package. That path is not exercised by in-tree tests (caller and bean
share a module, so privateLookupIn already has full privilege). This module is test-only
and is never published.</description>

<dependencies>
<dependency>
<groupId>tools.jackson.module</groupId>
<artifactId>jackson-module-blackbird</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<!-- This module is test-only and must never produce a publishable artifact.
Packaging stays `jar` so the compile + test lifecycle runs normally,
but every artifact-producing / artifact-publishing plugin inherited
from the parent chain is unbound here.

Verified via `mvn help:effective-pom -pl blackbird-tests`: every
execution listed below appears in the merged model at phase=none or
with skip=true. If you rename an execution here, re-verify with that
command — a typo turns into silent dead config and stale artifacts
in target/ rather than a build error. See afterburner-tests/pom.xml
for the same pattern with more detailed comments. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<executions>
<execution>
<id>bundle-manifest</id>
<phase>none</phase>
</execution>
<execution>
<id>default-install</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.gradlex</groupId>
<artifactId>gradle-module-metadata-maven-plugin</artifactId>
<executions>
<execution>
<id>default</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<!-- Force surefire to classpath mode: this module has no module-info.java,
so tests must run on the classpath (unnamed module). That's the whole
point — Blackbird's in-tree tests run as a named module and take the
easy privateLookupIn path that doesn't exercise CrossLoaderAccess. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading