Skip to content

Commit 97dd465

Browse files
committed
Add support for Jackson 3
Resolves #4842
1 parent d4bdf87 commit 97dd465

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<spring-ldap.version>4.0.0-M3</spring-ldap.version>
6868

6969
<jackson.version>2.19.2</jackson.version>
70+
<jackson3.version>3.0.0</jackson3.version>
7071
<avro.version>1.12.0</avro.version>
7172
<gson.version>2.13.1</gson.version>
7273
<hibernate-core.version>7.1.0.Final</hibernate-core.version>

spring-batch-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@
7272
<version>${jackson.version}</version>
7373
<optional>true</optional>
7474
</dependency>
75+
<dependency>
76+
<groupId>tools.jackson.core</groupId>
77+
<artifactId>jackson-databind</artifactId>
78+
<version>${jackson3.version}</version>
79+
<optional>true</optional>
80+
</dependency>
7581
<dependency>
7682
<groupId>com.fasterxml.jackson.datatype</groupId>
7783
<artifactId>jackson-datatype-jsr310</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.repository.dao;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.OutputStream;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
import tools.jackson.core.type.TypeReference;
25+
import tools.jackson.databind.json.JsonMapper;
26+
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
27+
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
28+
29+
import org.springframework.batch.core.repository.ExecutionContextSerializer;
30+
31+
/**
32+
* An {@link ExecutionContextSerializer} that uses Jackson 3 to serialize/deserialize the
33+
* execution context as JSON. By default, this serializer enables default typing with a
34+
* {@link BasicPolymorphicTypeValidator} that allows only classes from certain packages to
35+
* be deserialized, for security reasons. If you need a different configuration, you can
36+
* provide your own {@link JsonMapper} instance through the constructor.
37+
*
38+
* @author Mahmoud Ben Hassine
39+
* @since 6.0.0
40+
*/
41+
public class Jackson3ExecutionContextStringSerializer implements ExecutionContextSerializer {
42+
43+
private final JsonMapper jsonMapper;
44+
45+
/**
46+
* Create a new {@link Jackson3ExecutionContextStringSerializer} with default
47+
* configuration (only classes from certain packages will be allowed to be
48+
* deserialized).
49+
*/
50+
public Jackson3ExecutionContextStringSerializer() {
51+
PolymorphicTypeValidator polymorphicTypeValidator = BasicPolymorphicTypeValidator.builder()
52+
.allowIfSubType("java.util.")
53+
.allowIfSubType("java.sql.")
54+
.allowIfSubType("java.lang.")
55+
.allowIfSubType("java.math.")
56+
.allowIfSubType("java.time.")
57+
.allowIfSubType("java.net.")
58+
.allowIfSubType("java.xml.")
59+
.allowIfSubType("org.springframework.batch.")
60+
.build();
61+
this.jsonMapper = JsonMapper.builder().activateDefaultTyping(polymorphicTypeValidator).build();
62+
}
63+
64+
/**
65+
* Create a new {@link Jackson3ExecutionContextStringSerializer} with a custom
66+
* {@link JsonMapper}.
67+
* @param jsonMapper the {@link JsonMapper} to use for serialization/deserialization
68+
*/
69+
public Jackson3ExecutionContextStringSerializer(JsonMapper jsonMapper) {
70+
this.jsonMapper = jsonMapper;
71+
}
72+
73+
@Override
74+
public Map<String, Object> deserialize(InputStream inputStream) throws IOException {
75+
TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {
76+
};
77+
return this.jsonMapper.readValue(inputStream, typeRef);
78+
}
79+
80+
@Override
81+
public void serialize(Map<String, Object> object, OutputStream outputStream) throws IOException {
82+
this.jsonMapper.writeValue(outputStream, object);
83+
}
84+
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.repository;
17+
18+
import javax.sql.DataSource;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.batch.core.BatchStatus;
23+
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
24+
import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository;
25+
import org.springframework.batch.core.job.Job;
26+
import org.springframework.batch.core.job.JobExecution;
27+
import org.springframework.batch.core.job.builder.JobBuilder;
28+
import org.springframework.batch.core.job.parameters.JobParameters;
29+
import org.springframework.batch.core.launch.JobOperator;
30+
import org.springframework.batch.core.repository.dao.Jackson3ExecutionContextStringSerializer;
31+
import org.springframework.batch.core.scope.context.ChunkContext;
32+
import org.springframework.batch.core.step.Step;
33+
import org.springframework.batch.core.step.StepContribution;
34+
import org.springframework.batch.core.step.builder.StepBuilder;
35+
import org.springframework.batch.core.step.tasklet.Tasklet;
36+
import org.springframework.batch.infrastructure.repeat.RepeatStatus;
37+
import org.springframework.context.ApplicationContext;
38+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
39+
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.Configuration;
41+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
42+
import org.springframework.jdbc.support.JdbcTransactionManager;
43+
44+
import static org.junit.jupiter.api.Assertions.assertEquals;
45+
46+
public class Jackson3ExecutionContextStringSerializerIntegrationTests {
47+
48+
@Test
49+
void testExecutionContextSerializationDeserializationRoundTrip() throws Exception {
50+
// given
51+
ApplicationContext context = new AnnotationConfigApplicationContext(JobConfiguration.class);
52+
JobOperator jobOperator = context.getBean(JobOperator.class);
53+
Job job = context.getBean(Job.class);
54+
JobParameters jobParameters = new JobParameters();
55+
56+
// when
57+
JobExecution jobExecution = jobOperator.start(job, jobParameters);
58+
JobExecution restartedExecution = jobOperator.restart(jobExecution);
59+
60+
// then
61+
assertEquals(BatchStatus.FAILED, jobExecution.getStatus());
62+
assertEquals(true, jobExecution.getStepExecutions().iterator().next().getExecutionContext().get("failed"));
63+
assertEquals(BatchStatus.COMPLETED, restartedExecution.getStatus());
64+
assertEquals(false,
65+
restartedExecution.getStepExecutions().iterator().next().getExecutionContext().get("failed"));
66+
67+
}
68+
69+
@Configuration
70+
@EnableBatchProcessing
71+
@EnableJdbcJobRepository(executionContextSerializerRef = "serializer")
72+
static class JobConfiguration {
73+
74+
@Bean
75+
public Step step(JobRepository jobRepository, Tasklet tasklet) {
76+
return new StepBuilder("step", jobRepository).tasklet(tasklet).build();
77+
}
78+
79+
@Bean
80+
public Tasklet tasklet() {
81+
return new Tasklet() {
82+
private boolean shouldFail = true;
83+
84+
@Override
85+
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
86+
if (shouldFail) {
87+
shouldFail = false;
88+
contribution.getStepExecution().getExecutionContext().put("failed", true);
89+
throw new Exception("Expected failure");
90+
}
91+
System.out.println("Hello world!");
92+
contribution.getStepExecution().getExecutionContext().put("failed", false);
93+
return RepeatStatus.FINISHED;
94+
}
95+
};
96+
}
97+
98+
@Bean
99+
public Job job(JobRepository jobRepository, Step step) {
100+
return new JobBuilder("job", jobRepository).start(step).build();
101+
}
102+
103+
@Bean
104+
public Jackson3ExecutionContextStringSerializer serializer() {
105+
return new Jackson3ExecutionContextStringSerializer();
106+
}
107+
108+
@Bean
109+
public DataSource dataSource() {
110+
return new EmbeddedDatabaseBuilder().addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
111+
.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
112+
.generateUniqueName(true)
113+
.build();
114+
}
115+
116+
@Bean
117+
public JdbcTransactionManager transactionManager(DataSource dataSource) {
118+
return new JdbcTransactionManager(dataSource);
119+
}
120+
121+
}
122+
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.repository.dao;
17+
18+
import java.io.ByteArrayInputStream;
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.Serializable;
23+
import java.sql.Timestamp;
24+
import java.time.Instant;
25+
import java.time.LocalDate;
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
29+
import org.junit.jupiter.api.Test;
30+
import tools.jackson.databind.json.JsonMapper;
31+
32+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
35+
/**
36+
* @author Mahmoud Ben Hassine
37+
*/
38+
class Jackson3ExecutionContextStringSerializerTests {
39+
40+
record Person(int id, String name) implements Serializable {
41+
}
42+
43+
@Test
44+
void testSerializationDeserializationRoundTrip() throws IOException {
45+
// given
46+
JsonMapper jsonMapper = new JsonMapper();
47+
Jackson3ExecutionContextStringSerializer serializer = new Jackson3ExecutionContextStringSerializer(jsonMapper);
48+
Person person = new Person(1, "John Doe");
49+
Map<String, Object> context = new HashMap<>();
50+
context.put("person", person);
51+
52+
// when
53+
ByteArrayOutputStream os = new ByteArrayOutputStream();
54+
serializer.serialize(context, os);
55+
InputStream inputStream = new ByteArrayInputStream(os.toByteArray());
56+
57+
// then
58+
assertDoesNotThrow(() -> serializer.deserialize(inputStream));
59+
}
60+
61+
@Test
62+
void testSqlTimestampSerialization() throws IOException {
63+
// given
64+
Jackson3ExecutionContextStringSerializer serializer = new Jackson3ExecutionContextStringSerializer();
65+
Map<String, Object> context = new HashMap<>(1);
66+
Timestamp timestamp = new Timestamp(Instant.now().toEpochMilli());
67+
context.put("timestamp", timestamp);
68+
69+
// when
70+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
71+
serializer.serialize(context, outputStream);
72+
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
73+
Map<String, Object> deserializedContext = serializer.deserialize(inputStream);
74+
75+
// then
76+
Timestamp deserializedTimestamp = (Timestamp) deserializedContext.get("timestamp");
77+
assertEquals(timestamp, deserializedTimestamp);
78+
}
79+
80+
@Test
81+
void testJavaTimeLocalDateSerialization() throws IOException {
82+
// given
83+
Jackson3ExecutionContextStringSerializer serializer = new Jackson3ExecutionContextStringSerializer();
84+
Map<String, Object> map = new HashMap<>();
85+
LocalDate now = LocalDate.now();
86+
map.put("now", now);
87+
88+
// when
89+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
90+
serializer.serialize(map, outputStream);
91+
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
92+
Map<String, Object> deserializedContext = serializer.deserialize(inputStream);
93+
94+
// then
95+
LocalDate deserializedNow = (LocalDate) deserializedContext.get("now");
96+
assertEquals(now, deserializedNow);
97+
}
98+
99+
}

0 commit comments

Comments
 (0)