Skip to content
Open
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
21 changes: 19 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,24 @@ jobs:
matrix:
java_version: ['17', '21', '24', '25']
include:
- java_version: '17'
# Release/deploy runs on 25 so the blackbird module (profile-gated
# to JDK 25+) is part of the deployed reactor.
- java_version: '25'
release_build: 'R'
env:
JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Blackbird builds only on JDK 25+, but its jar must run on 17-24 too
# (module discovery skips it there); blackbird-tests starts those JDKs.
- name: Set up older JDKs for Blackbird runtime tests
if: ${{ matrix.java_version == '25' }}
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: 'temurin'
java-version: |
17
21
- name: Set up JDK
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
Expand All @@ -39,7 +51,12 @@ jobs:
# gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
# gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Build
run: ./mvnw -B -ff -ntp clean verify
run: |
OLD_JDKS=""
if [ -n "${JAVA_HOME_17_X64:-}" ] && [ -n "${JAVA_HOME_21_X64:-}" ]; then
OLD_JDKS="-Dblackbird.test.oldJdks=${JAVA_HOME_17_X64},${JAVA_HOME_21_X64}"
fi
./mvnw -B -ff -ntp clean verify ${OLD_JDKS}
- name: Extract project Maven version
id: projectVersion
run: echo "version=$(./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate -DforceStdout -Dexpression=project.version -q)" >> $GITHUB_OUTPUT
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ Every module follows the same pattern:
- A `*Module` class (e.g., `AfterburnerModule`, `BlackbirdModule`) extends Jackson's `tools.jackson.databind.JacksonModule`, registered on a mapper builder (e.g., `JsonMapper.builder().addModule(...)`)
- Package: `tools.jackson.module.<name>` (hyphens dropped or turned into sub-packages: `androidrecord`, `noctordeser`, `spisubtypes`, `jakarta.xmlbind`)
- `PackageVersion.java` is generated from `PackageVersion.java.in` by the replacer Maven plugin — do not edit manually
- JPMS module descriptors are real sources: `<module>/src/main/java/module-info.java` (and `src/test/java/module-info.java` for tests). Moditect is no longer used; stray `src/moditect/` dirs in blackbird and android-record are leftovers
- JPMS module descriptors are real sources: `<module>/src/main/java/module-info.java` (and `src/test/java/module-info.java` for tests). Moditect is no longer used; the stray `src/moditect/` dir in android-record is a leftover
- SPI registration: `src/main/resources/META-INF/services/tools.jackson.databind.JacksonModule` (all modules except guice, guice7, osgi)

### Key module categories

- **Performance (bytecode generation):** afterburner (Byte Buddy-based), blackbird (LambdaMetafactory-based, modern replacement) — both disable themselves automatically in GraalVM native images via `NativeImageUtil.isInNativeImage()` check in `setupModule`
- **Performance (bytecode generation):** afterburner (Byte Buddy-based), blackbird (generated per-bean codecs through the JDK ClassFile API; a multi-release jar that builds and runs on JDK 25+, see `blackbird/README.md`) — both disable themselves automatically in GraalVM native images via `NativeImageUtil.isInNativeImage()` check in `setupModule`
- **DI integration:** guice (javax.inject), guice7 (jakarta.inject) — note: guice's `ObjectMapperModule` implements Guice's `com.google.inject.Module`, not Jackson's `JacksonModule`
- **Type materialization:** mrbean (generates implementations for interfaces/abstract classes, Byte Buddy-based)
- **Annotation support:** jaxb (javax.xml.bind), jakarta-xmlbind (jakarta.xml.bind)
Expand Down
58 changes: 58 additions & 0 deletions blackbird-jpms-tests/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.3.0-SNAPSHOT</version>
</parent>
<artifactId>jackson-module-blackbird-jpms-tests</artifactId>
<name>Jackson module: Blackbird JPMS integration tests</name>
<packaging>jar</packaging>
<description>Module-path integration tests: a foreign named module that
requires blackbird, exercising codec generation for its own non-public
classes. Not published.</description>

<properties>
<maven.compiler.release>25</maven.compiler.release>
<javac.src.version>25</javac.src.version>
<javac.target.version>25</javac.target.version>
<!-- test-only module -->
<maven.deploy.skip>true</maven.deploy.skip>
<gpg.skip>true</gpg.skip>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- These tests assert module-path semantics (what a named module
can and cannot reach); the reactor-wide classpath toggle
(-Dsurefire.useModulePath=false) must not apply here. -->
<useModulePath>true</useModulePath>
<!-- The tests assert codec engagement through the generation trace. -->
<systemPropertyVariables>
<blackbird.debug.codegen>true</blackbird.debug.codegen>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>tools.jackson.module</groupId>
<artifactId>jackson-module-blackbird</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackbird.jpms.test.beans;

import java.lang.invoke.MethodHandles;

/** Hands tests this module's lookup and its non-public bean type. */
public final class Beans {
private Beans() {}

public static MethodHandles.Lookup lookup() {
return MethodHandles.lookup();
}

public static Class<?> pkgBeanClass() {
return PkgBean.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package blackbird.jpms.test.beans;

// Deliberately package-private: the codec for this bean must be defined in
// this module's package context, which only works when the generated class
// can resolve its supertype from here.
class PkgBean {
private int count;
private String name;

public int getCount() { return count; }
public void setCount(int v) { count = v; }
public String getName() { return name; }
public void setName(String v) { name = v; }
}
15 changes: 15 additions & 0 deletions blackbird-jpms-tests/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// A foreign named module from Blackbird's point of view: it requires the
// blackbird module and holds non-public beans plus a lookup of its own, the
// exact arrangement a modular application has.
module blackbird.jpms.test
{
requires tools.jackson.databind;
requires tools.jackson.module.blackbird;

exports blackbird.jpms.test.beans;
// What a modular application must do for stock databind to construct
// non-public beans reflectively - and the ONLY requirement blackbird
// adds: none. Deliberately NOT opened to blackbird: the accelerated path
// rides databind's fixAccess through unreflected constant handles.
opens blackbird.jpms.test.beans to tools.jackson.databind;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package blackbird.jpms.test.beans;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.module.blackbird.BlackbirdModule;

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

/**
* Runs on the module path as the named module blackbird.jpms.test, which
* requires blackbird: the arrangement a modular application has. A
* package-private bean of this module accelerates with no user lookup at all:
* the only JPMS requirement is the {@code opens ... to tools.jackson.databind}
* that stock databind already needs. Databind's fixAccess marks the members
* accessible, the codec reaches them through unreflected constant handles,
* and the generated class defines in blackbird's own module context. The
* lookup-supplying constructor remains a supported (now redundant) API.
*
* Engagement is asserted through the blackbird.debug.codegen diagnostic
* stream: byte-identical output makes generated and stock otherwise
* indistinguishable from out here.
*/
public class JpmsPackagePrivateTest
{
private static final String DOC = "{\"count\":7,\"name\":\"x\"}";

// The lookup API is a released contract and must keep working even though
// acceleration no longer needs it.
@Test
public void packagePrivateBeanAcceleratesWithModuleLookup() throws Exception {
ObjectMapper mapper = JsonMapper.builder()
.addModule(new BlackbirdModule() {
private static final long serialVersionUID = 1L;
@Override
protected Supplier<MethodHandles.Lookup> findLookupSupplier() {
return Beans::lookup;
}
})
.build();
String err = captureErr(() -> {
PkgBean bean = mapper.readValue(DOC, PkgBean.class);
assertEquals(7, bean.getCount());
assertEquals("x", bean.getName());
});
assertTrue(err.contains("BBReader_PkgBean"),
"expected a generated codec for PkgBean; diagnostics:\n" + err);
assertFalse(err.contains("null (gated)"),
"PkgBean was gated instead of accelerated; diagnostics:\n" + err);
}

@Test
public void acceleratesWithoutUserLookup() throws Exception {
// Widened from a demotion pin: no lookup is required any more. The
// opens-to-databind that stock needs is the whole contract.
ObjectMapper mapper = JsonMapper.builder()
.addModule(new BlackbirdModule())
.build();
String err = captureErr(() -> {
PkgBean bean = mapper.readValue(DOC, PkgBean.class);
assertEquals(7, bean.getCount());
assertEquals("x", bean.getName());
});
assertTrue(err.contains("BBReader_PkgBean"),
"expected a generated codec for PkgBean with no user lookup; diagnostics:\n"
+ err);
assertFalse(err.contains("null (gated)"),
"PkgBean was gated instead of accelerated; diagnostics:\n" + err);
}

private interface Body {
void run() throws Exception;
}

private static synchronized String captureErr(Body body) throws Exception {
PrintStream original = System.err;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setErr(new PrintStream(buffer, true, StandardCharsets.UTF_8));
try {
body.run();
} finally {
System.setErr(original);
}
return buffer.toString(StandardCharsets.UTF_8);
}
}
Loading
Loading