Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4107fce
Replace the Blackbird deserializer engine with per-bean generated cod…
Sep 6, 2026
f7e9db1
Correct the CD_BASE comment: the module-path failure was a test-build…
Sep 6, 2026
bb4f6bf
Add record support: typed-locals codec through the canonical constructor
Sep 6, 2026
1383580
Add builder support: fluent-setter codec through the stock instantiat…
Sep 6, 2026
bd96c3e
Add tier-B child linking, non-public setters via lookup handles, stoc…
Sep 6, 2026
2734225
Replace the Blackbird serializer engine with per-bean generated writers
Sep 6, 2026
3c818c5
Emit per-kind write helpers sized past the C2 inline threshold
Sep 7, 2026
8fe378b
Rewrite the README for the generated-codec engine
Sep 7, 2026
0dfd249
Rewrite blackbird-tests inject suite for the codec engine
Sep 7, 2026
694c786
Remove CrossLoaderAccess and the dead util chain (closes modules-base…
Sep 7, 2026
0babde9
Add generated field read/write access
Sep 7, 2026
e24a7af
Fix and promote the child-classloader test; harden the modifiers agai…
Sep 7, 2026
d7a9b13
Report codec-generation fallbacks; fail fast on generator bugs in tests
Sep 7, 2026
901bc59
Replace leftover fully qualified names with imports
Sep 7, 2026
31e4277
Restore stock behavior for coercion, unknown properties, record creat…
Sep 7, 2026
b8602cf
Gate blackbird modules on JDK 25+ and add a JDK 25 CI matrix entry
Sep 7, 2026
15a467b
Accelerate package-private and protected beans; wrap property errors …
Sep 7, 2026
bb271f8
Move generated-code supertypes to an exported .internal package
Sep 7, 2026
9d2dbf1
Pin the reactor's compiler release to the JDK 17 floor
Sep 7, 2026
c7faa2c
Add module-path integration tests: a foreign named module requiring b…
Sep 7, 2026
f0e52ad
Pin blackbird-jpms-tests to the module path
Sep 7, 2026
651cfc3
Document non-public class acceleration and the internal package in th…
Sep 8, 2026
4db1a08
Add a gated dump of generated class bytes for inspection
Sep 8, 2026
c32df01
Pin @JsonView parity through engaged codecs
Sep 8, 2026
e68b328
Keep view-active calls on the generated codec via a per-view mask
Sep 8, 2026
9afd22e
Name parameters and locals in generated classes
Sep 8, 2026
191ca2b
Emit view machinery only for beans that declare views
Sep 8, 2026
31774ae
Name classData entries through a base-class bootstrap
Sep 8, 2026
6c3551a
Rename deserializer-side classes for Reader/Writer symmetry
Sep 8, 2026
130e516
Move the stock-delegation tails after the generated bodies
Sep 8, 2026
e84b728
Accept PROPERTY_NAME entry in generated readers
Sep 8, 2026
c09f02c
Enforce FAIL_ON_NULL_CREATOR_PROPERTIES in record codecs; drop obsole…
Sep 8, 2026
bfb7827
Accelerate beans with ignored or included property sets
Sep 8, 2026
17eaacb
Accelerate case-insensitive mappers and property aliases
Sep 8, 2026
15d66eb
Pin the deliberate demotions: object ids, merge, any-setter, injectables
Sep 8, 2026
9860553
Replace leftover fully qualified names with imports
Sep 9, 2026
d7e4cf7
Construct only through the creator databind selected
Sep 10, 2026
6d90b65
Feed the stock any-setter from the unknown arm; construct through the…
Sep 10, 2026
5c06cf6
Route all member access through constant handles unreflected after fi…
Sep 11, 2026
6496636
Write polymorphic type wrappers natively; apply injected values after…
Sep 11, 2026
1e7c28b
Use JsonParser.nextNameMatchAndToken in generated deserializer loops
Sep 11, 2026
d3d2caa
Derive class descriptors from class literals; share the debug switches
Sep 12, 2026
91f99dc
Trace writer gates and modifier demotions
Sep 12, 2026
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
6 changes: 4 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: ['17', '21', '24']
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"
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package tools.jackson.module.blackbird.inject;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

Expand All @@ -11,25 +8,30 @@
import tools.jackson.databind.SerializationConfig;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.deser.SettableBeanProperty;
import tools.jackson.databind.deser.ValueDeserializerModifier;
import tools.jackson.databind.deser.bean.BeanDeserializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.ValueSerializerModifier;
import tools.jackson.module.blackbird.BlackbirdModule;

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

// Test utilities for verifying that Blackbird's lambda-based injection pipeline
// actually ran on a given POJO. Because Blackbird's optimized property and writer
// classes (SettableIntProperty, etc.; IntPropertyWriter, etc.) are package-private
// inside the blackbird module, these checks rely on reflection and simple-name
// matching rather than compile-time type references. Mirrors the afterburner-tests
// harness; see that module's README for the broader rationale.
// Test utilities for verifying that Blackbird's codec generation engaged for a
// given POJO loaded from the unnamed module (classpath). Blackbird installs a
// per-bean placeholder (BBReaderPlaceholder / BBWriterPlaceholder) that
// resolves to a generated hidden-class codec; the placeholder types are
// package-private inside the blackbird module, so these checks match by simple
// name and package rather than compile-time type references. Mirrors the
// afterburner-tests harness; see that module's README for the broader
// rationale.
abstract class BlackbirdInjectionTestBase
{
static {
// Strict mode: a codec-generation failure fails the test instead of
// demoting to the stock path (see CodegenFallbacks).
System.setProperty("tools.jackson.module.blackbird.failOnCodegenError", "true");
}

protected static Harness newHarness() {
return new Harness();
}
Expand Down Expand Up @@ -67,9 +69,11 @@ public ValueSerializer<?> modifySerializer(
});
}
};
// Modifiers run in reverse registration order, so the capture
// module registers first to observe what Blackbird installed.
this.mapper = JsonMapper.builder()
.addModule(new BlackbirdModule())
.addModule(capture)
.addModule(new BlackbirdModule())
.build();
}

Expand All @@ -86,86 +90,20 @@ ValueSerializer<?> serFor(Class<?> cls) {
}
}

/** Returns the `_propsByIndex` array from a bean deserializer. */
protected static SettableBeanProperty[] propsOf(ValueDeserializer<?> deser) {
if (!(deser instanceof BeanDeserializer)) {
throw new AssertionError("not a BeanDeserializer: " + deser.getClass().getName());
}
return (SettableBeanProperty[]) reflectField(deser, "_propsByIndex");
/** True if Blackbird installed its deserializer codec for the captured value. */
protected static boolean isBlackbirdReader(ValueDeserializer<?> deser) {
return blackbirdClassChainIncludes(deser.getClass(), "BBReaderPlaceholder");
}

/** Returns the BeanPropertyWriter[] from a bean serializer, as a list. */
protected static List<BeanPropertyWriter> writersOf(ValueSerializer<?> ser) {
BeanPropertyWriter[] arr = (BeanPropertyWriter[]) reflectField(ser, "_props");
List<BeanPropertyWriter> out = new ArrayList<>(arr.length);
for (BeanPropertyWriter w : arr) {
out.add(w);
}
return out;
}

/** Walks the class chain of {@code instance} looking for a declared field
* named {@code fieldName}. Picks the error message hypothesis based on the
* class's package — databind/blackbird fields usually mean a rename,
* anything else means the caller passed the wrong receiver. */
protected static Object reflectField(Object instance, String fieldName) {
Class<?> origClass = instance.getClass();
Class<?> c = origClass;
while (c != null) {
try {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
return f.get(instance);
} catch (NoSuchFieldException ignore) {
c = c.getSuperclass();
} catch (IllegalAccessException e) {
throw new AssertionError("cannot read field '" + fieldName + "' on "
+ origClass.getName(), e);
}
}
String pkg = origClass.getPackageName();
String hint;
if (pkg.startsWith("tools.jackson.databind")
|| pkg.startsWith("tools.jackson.module.blackbird")) {
hint = "databind or blackbird may have renamed or removed it;"
+ " update " + BlackbirdInjectionTestBase.class.getSimpleName()
+ " to match.";
} else {
hint = "this looks like the wrong receiver type — '" + fieldName
+ "' is an internal Jackson field and the caller passed an"
+ " instance of " + origClass.getName() + ".";
}
throw new AssertionError("field '" + fieldName + "' not found on "
+ origClass.getName() + " (walked up full class chain) — " + hint);
}

/** True if `prop`'s class chain contains Blackbird's OptimizedSettableBeanProperty. */
protected static boolean isOptimizedProperty(SettableBeanProperty prop) {
return blackbirdClassChainIncludes(prop.getClass(), "OptimizedSettableBeanProperty");
}

/** True if `writer`'s class chain contains Blackbird's OptimizedBeanPropertyWriter. */
protected static boolean isOptimizedWriter(BeanPropertyWriter writer) {
return blackbirdClassChainIncludes(writer.getClass(), "OptimizedBeanPropertyWriter");
}

/** Walks the superclass chain of {@code cls} looking for a class whose simple
* name is {@code simpleName}. Used to recognize Blackbird's package-private
* optimized types without importing them. */
protected static boolean classChainIncludes(Class<?> cls, String simpleName) {
Class<?> c = cls;
while (c != null) {
if (simpleName.equals(c.getSimpleName())) {
return true;
}
c = c.getSuperclass();
}
return false;
/** True if Blackbird installed its serializer codec for the captured value. */
protected static boolean isBlackbirdWriter(ValueSerializer<?> ser) {
return blackbirdClassChainIncludes(ser.getClass(), "BBWriterPlaceholder");
}

/** Like {@link #classChainIncludes} but additionally requires the matched
* class to live inside a blackbird package. Guards against false positives
* from unrelated classes that happen to share a simple name. */
/** Walks the superclass chain of {@code cls} looking for a class whose
* simple name is {@code simpleName} and that lives inside a blackbird
* package. Recognizes Blackbird's package-private types without importing
* them, and guards against unrelated classes sharing a simple name. */
protected static boolean blackbirdClassChainIncludes(Class<?> cls, String simpleName) {
Class<?> c = cls;
while (c != null) {
Expand Down
Loading
Loading