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
14 changes: 8 additions & 6 deletions temporal-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {
java17Implementation files(sourceSets.main.output.classesDirs) { builtBy compileJava }
java17Implementation files({ sourceSets.main.compileClasspath })
java17CompileOnly "tools.jackson.core:jackson-databind:$jackson3Version"
// The Java 8 stub must declare the same public API as the Java 17 class (JEP 238), so it needs
// the Jackson 3 types at compile time only.
compileOnly "tools.jackson.core:jackson-databind:$jackson3Version"

java21Implementation files(sourceSets.main.output.classesDirs) { builtBy compileJava }
}
Expand Down Expand Up @@ -181,9 +184,8 @@ test {
}

// On Java 17+, prepend java17 classes to all test classpaths so that Class.forName finds
// the real Jackson3JsonPayloadConverter instead of the Java 8 stub. This lets us test
// the present-java17-but-absent-jackson3 behavior (NoClassDefFoundError) in the same
// test that tests the Java 8 stub behavior (UnsupportedOperationException).
// the real Jackson3JsonPayloadConverter instead of the Java 8 stub, matching what the
// multi-release jar resolves to at runtime.
tasks.withType(Test).configureEach {
dependsOn compileJava17Java
doFirst {
Expand Down Expand Up @@ -236,9 +238,8 @@ testing {
jackson3Tests(JvmTestSuite) {
dependencies {
// java17 output must come before project() (added by configureEach) so that
// the compiler and runtime see the real Jackson3JsonPayloadConverter — which
// has a wider API than the Java 8 stub (newDefaultJsonMapper, JsonMapper
// constructor) because the stub can't reference Jackson 3 types.
// the compiler and runtime see the real Jackson3JsonPayloadConverter rather
// than the Java 8 stub, which only throws.
implementation files(sourceSets.java17.output.classesDirs) { builtBy compileJava17Java }
implementation "tools.jackson.core:jackson-databind:$jackson3Version"
}
Expand All @@ -250,6 +251,7 @@ testing {
languageVersion = JavaLanguageVersion.of(project.property("testJavaVersion") as int)
}
}
systemProperty 'temporal.sdk.mainClassesDirs', sourceSets.main.output.classesDirs.asPath
shouldRunAfter(test)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.temporal.common.converter;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.util.Set;
import java.util.TreeSet;
import org.junit.Test;

/**
* Verifies that the Java 8 stub of {@link Jackson3JsonPayloadConverter} declares the same public
* API as the Java 17 class, as required for versioned entries of a multi-release jar. Tools that
* compile against the base entry otherwise cannot resolve members that exist only in the Java 17
* class.
*/
public class Jackson3JsonPayloadConverterApiTest {

private static final String CLASS_FILE =
Jackson3JsonPayloadConverter.class.getName().replace('.', '/') + ".class";

@Test
public void testStubDeclaresTheSamePublicApi() throws Exception {
byte[] stub = readStub();
Class<?> stubClass =
new ClassLoader(getClass().getClassLoader()) {
Class<?> define() {
return defineClass(null, stub, 0, stub.length);
}
}.define();

assertEquals(publicApi(Jackson3JsonPayloadConverter.class), publicApi(stubClass));
}

private static byte[] readStub() throws IOException {
String dirs = System.getProperty("temporal.sdk.mainClassesDirs");
for (String dir : dirs.split(File.pathSeparator, -1)) {
File classFile = new File(dir, CLASS_FILE);
if (classFile.isFile()) {
return Files.readAllBytes(classFile.toPath());
}
}
throw new AssertionError("stub not found in " + dirs);
}

private static Set<String> publicApi(Class<?> clazz) {
Set<String> api = new TreeSet<>();
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (Modifier.isPublic(constructor.getModifiers())) {
api.add(constructor.toGenericString());
}
}
for (Method method : clazz.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers()) && !method.isSynthetic()) {
api.add(method.toGenericString());
}
}
return api;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.temporal.common.Experimental;
import java.lang.reflect.Type;
import java.util.Optional;
import tools.jackson.databind.json.JsonMapper;

/**
* A {@link PayloadConverter} that uses Jackson 3.x for JSON serialization/deserialization. This
Expand Down Expand Up @@ -32,6 +33,14 @@ public Jackson3JsonPayloadConverter(boolean jackson2Compat) {
throw new UnsupportedOperationException(UNSUPPORTED_MSG);
}

public Jackson3JsonPayloadConverter(JsonMapper mapper) {
throw new UnsupportedOperationException(UNSUPPORTED_MSG);
}

public static JsonMapper newDefaultJsonMapper(boolean jackson2Compat) {
throw new UnsupportedOperationException(UNSUPPORTED_MSG);
}

@Override
public String getEncodingType() {
throw new UnsupportedOperationException(UNSUPPORTED_MSG);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import static org.junit.Assert.fail;

import io.temporal.api.common.v1.Payloads;
import java.lang.reflect.InvocationTargetException;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -25,27 +24,11 @@ public void testSetDefaultAsJackson3ThrowsWithoutJackson3() {
JacksonJsonPayloadConverter.setDefaultAsJackson3(true, true);
fail("Expected IllegalStateException");
} catch (IllegalStateException e) {
// On Java 8: Class.forName finds the stub, whose constructor throws
// UnsupportedOperationException → wrapped in InvocationTargetException by reflection.
// On Java 17+: Class.forName finds the real impl (java17 classes are on the classpath)
// but Jackson 3 types are absent, so class loading throws NoClassDefFoundError directly.
// Both the Java 8 stub and the Java 17 class reference Jackson 3 types, so reflective
// constructor lookup fails while linking when Jackson 3 is absent.
Throwable cause = e.getCause();
String specVersion = System.getProperty("java.specification.version");
int majorVersion =
specVersion.startsWith("1.")
? Integer.parseInt(specVersion.substring(2))
: Integer.parseInt(specVersion);
if (majorVersion >= 17) {
assertTrue(
"Expected NoClassDefFoundError, got: " + cause, cause instanceof NoClassDefFoundError);
} else {
assertTrue(
"Expected InvocationTargetException, got: " + cause,
cause instanceof InvocationTargetException);
assertTrue(
"Expected UnsupportedOperationException, got: " + cause.getCause(),
cause.getCause() instanceof UnsupportedOperationException);
}
assertTrue(
"Expected NoClassDefFoundError, got: " + cause, cause instanceof NoClassDefFoundError);
}
}

Expand Down
Loading