diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 95e8ccf8d..2438cb3d3 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -49,17 +49,18 @@ jobs: strategy: fail-fast: false matrix: - jdk: [8, 9] + # github not support JVM 8 anymore + jdk: [11, 17] optimizely_default_parser: [GSON_CONFIG_PARSER, JACKSON_CONFIG_PARSER, JSON_CONFIG_PARSER, JSON_SIMPLE_CONFIG_PARSER] steps: - name: checkout uses: actions/checkout@v4 - name: set up JDK ${{ matrix.jdk }} - uses: AdoptOpenJDK/install-jdk@v1 + uses: actions/setup-java@v4 with: - version: ${{ matrix.jdk }} - architecture: x64 + java-version: ${{ matrix.jdk }} + distribution: 'temurin' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/README.md b/README.md index 1a7370c43..e5d88473f 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,9 @@ dependencies { compile 'com.optimizely.ab:core-api:{VERSION}' compile 'com.optimizely.ab:core-httpclient-impl:{VERSION}' // The SDK integrates with multiple JSON parsers, here we use Jackson. - compile 'com.fasterxml.jackson.core:jackson-core:2.7.1' - compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.1' - compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1' + compile 'com.fasterxml.jackson.core:jackson-core:2.13.5' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.13.5' + compile 'com.fasterxml.jackson.core:jackson-databind:2.13.5' } ``` diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JacksonSerializer.java b/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JacksonSerializer.java index 6087b4cce..1467a0fa4 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JacksonSerializer.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JacksonSerializer.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, 2019, Optimizely and contributors + * Copyright 2016-2017, 2019, 2025 Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,41 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; class JacksonSerializer implements Serializer { - private ObjectMapper mapper = - new ObjectMapper().setPropertyNamingStrategy( - PropertyNamingStrategy.SNAKE_CASE); + private ObjectMapper mapper = createMapper(); + + /** + * Creates an ObjectMapper with snake_case naming strategy. + * Supports both Jackson 2.12+ (PropertyNamingStrategies) and earlier versions (PropertyNamingStrategy). + * Uses reflection to avoid compile-time dependencies on either API. + */ + static ObjectMapper createMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + Object namingStrategy = getSnakeCaseStrategy(); + objectMapper.setPropertyNamingStrategy((com.fasterxml.jackson.databind.PropertyNamingStrategy) namingStrategy); + return objectMapper; + } + + /** + * Gets the snake case naming strategy, supporting both Jackson 2.12+ and earlier versions. + */ + private static Object getSnakeCaseStrategy() { + try { + // Try Jackson 2.12+ API first + Class strategiesClass = Class.forName("com.fasterxml.jackson.databind.PropertyNamingStrategies"); + return strategiesClass.getField("SNAKE_CASE").get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + try { + // Fall back to Jackson 2.11 and earlier (deprecated but compatible) + Class strategyClass = Class.forName("com.fasterxml.jackson.databind.PropertyNamingStrategy"); + return strategyClass.getField("SNAKE_CASE").get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ex) { + throw new RuntimeException("Unable to find snake_case naming strategy in Jackson", ex); + } + } + } public String serialize(T payload) { mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/JacksonSerializerTest.java b/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/JacksonSerializerTest.java index fb068e3ab..139e0db66 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/JacksonSerializerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/JacksonSerializerTest.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.optimizely.ab.event.internal.payload.EventBatch; import org.junit.Test; @@ -36,14 +35,34 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; public class JacksonSerializerTest { private JacksonSerializer serializer = new JacksonSerializer(); - private ObjectMapper mapper = - new ObjectMapper().setPropertyNamingStrategy( - PropertyNamingStrategy.SNAKE_CASE); + private ObjectMapper mapper = JacksonSerializer.createMapper(); + @Test + public void createMapperSucceeds() { + // Verify that createMapper() successfully creates an ObjectMapper with snake_case naming + // This tests that the reflection logic works for the current Jackson version + ObjectMapper testMapper = JacksonSerializer.createMapper(); + assertNotNull("Mapper should be created successfully", testMapper); + + // Verify snake_case naming by serializing a simple object + class TestObject { + @SuppressWarnings("unused") + public String getMyFieldName() { return "test"; } + } + + try { + String json = testMapper.writeValueAsString(new TestObject()); + assertTrue("Should use snake_case naming", json.contains("my_field_name")); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize with snake_case naming", e); + } + } @Test public void serializeImpression() throws IOException { diff --git a/java-quickstart/build.gradle b/java-quickstart/build.gradle index a58fb090e..ef86b3045 100644 --- a/java-quickstart/build.gradle +++ b/java-quickstart/build.gradle @@ -4,7 +4,11 @@ dependencies { implementation project(':core-api') implementation project(':core-httpclient-impl') - implementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion + // implementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion + implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' + implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: httpClientVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4jVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4jVersion