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
1 change: 1 addition & 0 deletions conjure-java-jackson-optimizations/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ apply plugin: 'com.palantir.revapi'

dependencies {
api "com.fasterxml.jackson.core:jackson-databind"
implementation "com.fasterxml.jackson.module:jackson-module-blackbird"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.palantir.conjure.java.jackson.optimizations;

import com.fasterxml.jackson.module.blackbird.BlackbirdModule;
import java.util.List;

/**
Expand All @@ -25,11 +26,29 @@
*/
public final class ObjectMapperOptimizations {

// Blackbird generates classes at runtime via LambdaMetafactory, which native images cannot do.
private static final boolean NATIVE_IMAGE = System.getProperty("org.graalvm.nativeimage.imagecode") != null;

/** Equivalent to {@link #createModules(boolean) createModules(false)}: no optimization modules. */
public static List<? extends com.fasterxml.jackson.databind.Module> createModules() {
// At one point this conditionally returned List.of(new AfterburnerModule), however afterburner is not
// supported on any supported LTS Java release anymore, and the Blackbird alternative incurs a memory
// leak (https://github.com/FasterXML/jackson-modules-base/issues/147).
return List.of();
return createModules(false);
}

/**
* Optionally registers the Blackbird module, which speeds up (de)serialization by generating accessors at
* runtime rather than using reflection. At one point this returned an {@code AfterburnerModule}, however
* afterburner is not supported on any supported LTS Java release anymore.
*
* <p>Blackbird generates a class per accessor via {@code LambdaMetafactory}. This is bounded and safe when a
* service reuses a small set of long-lived {@code ObjectMapper}s, but creating mappers per-request leaks
* compressed class space (https://github.com/FasterXML/jackson-modules-base/issues/147), so it is opt-in.
* Optimizations are always disabled within native images, where runtime class generation is unsupported.
*/
public static List<? extends com.fasterxml.jackson.databind.Module> createModules(boolean useBlackbird) {
if (!useBlackbird || NATIVE_IMAGE) {
return List.of();
}
return List.of(new BlackbirdModule());
}

private ObjectMapperOptimizations() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ private ObjectMappers() {}
* </ul>
*/
public static JsonMapper newClientJsonMapper() {
return withDefaultModules(JsonMapper.builder(jsonFactory()))
return newClientJsonMapper(false);
}

/** Like {@link #newClientJsonMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static JsonMapper newClientJsonMapper(boolean useOptimizations) {
return withDefaultModules(JsonMapper.builder(jsonFactory()), useOptimizations)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -65,7 +70,12 @@ public static JsonMapper newClientJsonMapper() {
* </ul>
*/
public static CBORMapper newClientCborMapper() {
return withDefaultModules(CBORMapper.builder(cborFactory()))
return newClientCborMapper(false);
}

/** Like {@link #newClientCborMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static CBORMapper newClientCborMapper(boolean useOptimizations) {
return withDefaultModules(CBORMapper.builder(cborFactory()), useOptimizations)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -81,7 +91,12 @@ public static CBORMapper newClientCborMapper() {
* </ul>
*/
public static SmileMapper newClientSmileMapper() {
return withDefaultModules(SmileMapper.builder(smileFactory()))
return newClientSmileMapper(false);
}

/** Like {@link #newClientSmileMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static SmileMapper newClientSmileMapper(boolean useOptimizations) {
return withDefaultModules(SmileMapper.builder(smileFactory()), useOptimizations)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -96,7 +111,12 @@ public static SmileMapper newClientSmileMapper() {
* </ul>
*/
public static JsonMapper newServerJsonMapper() {
return withDefaultModules(JsonMapper.builder(jsonFactory()))
return newServerJsonMapper(false);
}

/** Like {@link #newServerJsonMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static JsonMapper newServerJsonMapper(boolean useOptimizations) {
return withDefaultModules(JsonMapper.builder(jsonFactory()), useOptimizations)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -111,7 +131,12 @@ public static JsonMapper newServerJsonMapper() {
* </ul>
*/
public static CBORMapper newServerCborMapper() {
return withDefaultModules(CBORMapper.builder(cborFactory()))
return newServerCborMapper(false);
}

/** Like {@link #newServerCborMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static CBORMapper newServerCborMapper(boolean useOptimizations) {
return withDefaultModules(CBORMapper.builder(cborFactory()), useOptimizations)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -127,7 +152,12 @@ public static CBORMapper newServerCborMapper() {
* </ul>
*/
public static SmileMapper newServerSmileMapper() {
return withDefaultModules(SmileMapper.builder(smileFactory()))
return newServerSmileMapper(false);
}

/** Like {@link #newServerSmileMapper()}, optionally enabling Jackson optimization modules (Blackbird). */
public static SmileMapper newServerSmileMapper(boolean useOptimizations) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have other optimizations in mind down the line?

I wonder if we should name this something like "newSingleInstantiatedServerSmileMapper" or something to make it clear that this should not be called multiple times, and then enable blackbird by default for this case.

Maybe we can write an errorprone check(? e.g. only created in a final variable in a class - though this is a bit restrictive)

return withDefaultModules(SmileMapper.builder(smileFactory()), useOptimizations)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand Down Expand Up @@ -170,13 +200,22 @@ public static ObjectMapper newSmileServerObjectMapper() {
* <li>Deserializing a null for a primitive field will throw an exception.
* </ul>
*/
@SuppressWarnings("for-rollout:deprecation")
public static <M extends ObjectMapper, B extends MapperBuilder<M, B>> B withDefaultModules(B builder) {
return withDefaultModules(builder, false);
}

/**
* Like {@link #withDefaultModules(MapperBuilder)}, optionally enabling Jackson optimization modules (Blackbird).
* See {@link ObjectMapperOptimizations#createModules(boolean)} for the tradeoffs of enabling optimizations.
*/
@SuppressWarnings("for-rollout:deprecation")
public static <M extends ObjectMapper, B extends MapperBuilder<M, B>> B withDefaultModules(
B builder, boolean useOptimizations) {
return builder.typeFactory(NonCachingTypeFactory.from(builder.build().getTypeFactory()))
.addModule(new GuavaModule())
.addModule(new ShimJdk7Module())
.addModule(new Jdk8Module().configureAbsentsAsNulls(true))
.addModules(ObjectMapperOptimizations.createModules())
.addModules(ObjectMapperOptimizations.createModules(useOptimizations))
.addModule(new JavaTimeModule())
.addModule(new LenientLongModule())
// we strongly recommend using built-in java.time classes instead of joda ones. Joda deserialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.palantir.conjure.java.jackson.optimizations.ObjectMapperOptimizations;
import com.palantir.logsafe.Preconditions;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;
Expand All @@ -50,6 +51,7 @@
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -511,6 +513,40 @@ public void testExtraordinarilyLargeStrings() throws IOException {
assertThat(parsed).hasSize(size);
}

@Test
public void optimizationsDisabledByDefault() {
assertThat(ObjectMapperOptimizations.createModules()).isEmpty();
assertThat(ObjectMapperOptimizations.createModules(false)).isEmpty();
assertThat(hasBlackbird(ObjectMappers.newServerJsonMapper()))
.as("Blackbird should not be registered by default")
.isFalse();
}

@Test
public void blackbirdOptimizationRegistersModuleAndPreservesOutput() throws IOException {
assertThat(ObjectMapperOptimizations.createModules(true)).hasSize(1);

JsonMapper optimized = ObjectMappers.newServerJsonMapper(true);
assertThat(hasBlackbird(optimized))
.as("Blackbird should be registered when optimizations are enabled")
.isTrue();
assertThat(hasBlackbird(ObjectMappers.newClientJsonMapper(true)))
.as("Blackbird should be registered on client mappers too")
.isTrue();

// Blackbird only changes how (de)serialization is performed, not the wire output.
Map<String, Object> value = Map.of("name", "witchcraft", "count", 42);
String optimizedJson = optimized.writeValueAsString(value);
assertThat(optimizedJson).isEqualTo(ObjectMappers.newServerJsonMapper().writeValueAsString(value));
assertThat(optimized.readValue(optimizedJson, new TypeReference<Map<String, Object>>() {}))
.isEqualTo(value);
}

private static boolean hasBlackbird(ObjectMapper mapper) {
return mapper.getRegisteredModuleIds().stream()
.anyMatch(id -> id.toString().toLowerCase(Locale.ROOT).contains("blackbird"));
}

private static String ser(Object object) throws IOException {
return MAPPER.writeValueAsString(object);
}
Expand Down
6 changes: 4 additions & 2 deletions versions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ com.fasterxml:classmate:1.5.1 (1 constraints: 9a122a13)

com.fasterxml.jackson.core:jackson-annotations:2.21 (12 constraints: 03de1b79)

com.fasterxml.jackson.core:jackson-core:2.21.4 (14 constraints: d1231840)
com.fasterxml.jackson.core:jackson-core:2.21.4 (15 constraints: 503a606a)

com.fasterxml.jackson.core:jackson-databind:2.21.4 (18 constraints: c86371a8)
com.fasterxml.jackson.core:jackson-databind:2.21.4 (19 constraints: 477ad144)

com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.4 (2 constraints: 2f204c06)

Expand All @@ -24,6 +24,8 @@ com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.21.4 (1 constraints:

com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-cbor-provider:2.21.4 (1 constraints: 3b053a3b)

com.fasterxml.jackson.module:jackson-module-blackbird:2.21.4 (1 constraints: 3b053a3b)

com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.4 (2 constraints: e53097b3)

com.fasterxml.jackson.module:jackson-module-scala_2.12:2.21.4 (1 constraints: 3b053a3b)
Expand Down