Skip to content

Commit 6d7d292

Browse files
committed
Update Jackson2ExecutionContextStringSerializer
1 parent 6052852 commit 6d7d292

File tree

3 files changed

+319
-6
lines changed

3 files changed

+319
-6
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/Jackson2ExecutionContextStringSerializer.java

Lines changed: 211 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2008-2019 the original author or authors.
2+
* Copyright 2008-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,28 +18,80 @@
1818
import java.io.IOException;
1919
import java.io.InputStream;
2020
import java.io.OutputStream;
21+
import java.util.Arrays;
22+
import java.util.Collection;
23+
import java.util.Collections;
2124
import java.util.Date;
2225
import java.util.HashMap;
26+
import java.util.HashSet;
2327
import java.util.Map;
28+
import java.util.Set;
2429

30+
import com.fasterxml.jackson.annotation.JacksonAnnotation;
2531
import com.fasterxml.jackson.annotation.JsonIgnore;
32+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2633
import com.fasterxml.jackson.core.JsonParser;
2734
import com.fasterxml.jackson.core.type.TypeReference;
35+
import com.fasterxml.jackson.databind.DatabindContext;
36+
import com.fasterxml.jackson.databind.DeserializationConfig;
2837
import com.fasterxml.jackson.databind.DeserializationContext;
2938
import com.fasterxml.jackson.databind.DeserializationFeature;
39+
import com.fasterxml.jackson.databind.JavaType;
3040
import com.fasterxml.jackson.databind.JsonNode;
3141
import com.fasterxml.jackson.databind.MapperFeature;
3242
import com.fasterxml.jackson.databind.ObjectMapper;
33-
43+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
3444
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
45+
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
46+
import com.fasterxml.jackson.databind.jsontype.NamedType;
47+
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
48+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
49+
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
3550
import com.fasterxml.jackson.databind.module.SimpleModule;
51+
3652
import org.springframework.batch.core.JobParameter;
3753
import org.springframework.batch.core.JobParameters;
3854
import org.springframework.batch.core.repository.ExecutionContextSerializer;
55+
import org.springframework.core.annotation.AnnotationUtils;
3956
import org.springframework.util.Assert;
4057

4158
/**
42-
* Implementation that uses Jackson2 to provide (de)serialization.
59+
* Implementation that uses Jackson2 to provide (de)serialization.
60+
*
61+
* By default, this implementation trusts a limited set of classes to be
62+
* deserialized from the execution context. If a class is not trusted by default
63+
* and is safe to deserialize, you can provide an explicit mapping using Jackson
64+
* annotations, as shown in the following example:
65+
*
66+
* <pre class="code">
67+
* &#064;JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
68+
* public class MyTrustedType implements Serializable {
69+
*
70+
* }
71+
* </pre>
72+
*
73+
* It is also possible to provide a custom {@link ObjectMapper} with a mixin for
74+
* the trusted type:
75+
*
76+
* <pre class="code">
77+
* ObjectMapper objectMapper = new ObjectMapper();
78+
* objectMapper.addMixIn(MyTrustedType.class, Object.class);
79+
* Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
80+
* serializer.setObjectMapper(objectMapper);
81+
* // register serializer in JobRepositoryFactoryBean
82+
* </pre>
83+
*
84+
* If the (de)serialization is only done by a trusted source, you can also enable
85+
* default typing:
86+
*
87+
* <pre class="code">
88+
* PolymorphicTypeValidator polymorphicTypeValidator = .. // configure your trusted PolymorphicTypeValidator
89+
* ObjectMapper objectMapper = new ObjectMapper();
90+
* objectMapper.activateDefaultTyping(polymorphicTypeValidator);
91+
* Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
92+
* serializer.setObjectMapper(objectMapper);
93+
* // register serializer in JobRepositoryFactoryBean
94+
* </pre>
4395
*
4496
* @author Marten Deinum
4597
* @author Mahmoud Ben Hassine
@@ -55,7 +107,8 @@ public Jackson2ExecutionContextStringSerializer() {
55107
this.objectMapper = new ObjectMapper();
56108
this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
57109
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
58-
this.objectMapper.enableDefaultTyping();
110+
this.objectMapper.configure(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES, true);
111+
this.objectMapper.setDefaultTyping(createTrustedDefaultTyping());
59112
this.objectMapper.registerModule(new JobParametersModule());
60113
}
61114

@@ -141,4 +194,158 @@ public JobParameter deserialize(JsonParser parser, DeserializationContext contex
141194

142195
}
143196

197+
/**
198+
* Creates a TypeResolverBuilder that checks if a type is trusted.
199+
* @return a TypeResolverBuilder that checks if a type is trusted.
200+
*/
201+
private static TypeResolverBuilder<? extends TypeResolverBuilder> createTrustedDefaultTyping() {
202+
TypeResolverBuilder<? extends TypeResolverBuilder> result = new TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL);
203+
result = result.init(JsonTypeInfo.Id.CLASS, null);
204+
result = result.inclusion(JsonTypeInfo.As.PROPERTY);
205+
return result;
206+
}
207+
208+
/**
209+
* An implementation of {@link ObjectMapper.DefaultTypeResolverBuilder}
210+
* that inserts an {@code allow all} {@link PolymorphicTypeValidator}
211+
* and overrides the {@code TypeIdResolver}
212+
* @author Rob Winch
213+
*/
214+
static class TrustedTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
215+
216+
TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) {
217+
super(
218+
defaultTyping,
219+
//we do explicit validation in the TypeIdResolver
220+
BasicPolymorphicTypeValidator.builder()
221+
.allowIfSubType(Object.class)
222+
.build()
223+
);
224+
}
225+
226+
@Override
227+
protected TypeIdResolver idResolver(MapperConfig<?> config,
228+
JavaType baseType,
229+
PolymorphicTypeValidator subtypeValidator,
230+
Collection<NamedType> subtypes, boolean forSer, boolean forDeser) {
231+
TypeIdResolver result = super.idResolver(config, baseType, subtypeValidator, subtypes, forSer, forDeser);
232+
return new TrustedTypeIdResolver(result);
233+
}
234+
}
235+
236+
/**
237+
* A {@link TypeIdResolver} that delegates to an existing implementation and throws an IllegalStateException if the
238+
* class being looked up is not trusted, does not provide an explicit mixin, and is not annotated with Jackson
239+
* mappings.
240+
*/
241+
static class TrustedTypeIdResolver implements TypeIdResolver {
242+
private static final Set<String> TRUSTED_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList(
243+
"java.util.ArrayList",
244+
"java.util.LinkedList",
245+
"java.util.Collections$EmptyList",
246+
"java.util.Collections$EmptyMap",
247+
"java.util.Collections$EmptySet",
248+
"java.util.Collections$UnmodifiableRandomAccessList",
249+
"java.util.Collections$UnmodifiableList",
250+
"java.util.Collections$UnmodifiableMap",
251+
"java.util.Collections$UnmodifiableSet",
252+
"java.util.Collections$SingletonList",
253+
"java.util.Collections$SingletonMap",
254+
"java.util.Collections$SingletonSet",
255+
"java.util.Date",
256+
"java.time.Instant",
257+
"java.time.Duration",
258+
"java.time.LocalDate",
259+
"java.time.LocalTime",
260+
"java.time.LocalDateTime",
261+
"java.net.URL",
262+
"java.util.TreeMap",
263+
"java.util.HashMap",
264+
"java.util.LinkedHashMap",
265+
"java.util.TreeSet",
266+
"java.util.HashSet",
267+
"java.util.LinkedHashSet",
268+
"java.lang.Boolean",
269+
"java.lang.Byte",
270+
"java.lang.Short",
271+
"java.lang.Integer",
272+
"java.lang.Long",
273+
"java.lang.Double",
274+
"java.lang.Float",
275+
"java.math.BigDecimal",
276+
"java.math.BigInteger",
277+
"java.lang.String",
278+
"java.lang.Character",
279+
"java.lang.CharSequence",
280+
"java.util.Properties",
281+
"[Ljava.util.Properties;",
282+
"org.springframework.batch.core.JobParameter",
283+
"org.springframework.batch.core.JobParameters",
284+
"org.springframework.batch.core.jsr.partition.JsrPartitionHandler$PartitionPlanState"
285+
)));
286+
287+
private final TypeIdResolver delegate;
288+
289+
TrustedTypeIdResolver(TypeIdResolver delegate) {
290+
this.delegate = delegate;
291+
}
292+
293+
@Override
294+
public void init(JavaType baseType) {
295+
delegate.init(baseType);
296+
}
297+
298+
@Override
299+
public String idFromValue(Object value) {
300+
return delegate.idFromValue(value);
301+
}
302+
303+
@Override
304+
public String idFromValueAndType(Object value, Class<?> suggestedType) {
305+
return delegate.idFromValueAndType(value, suggestedType);
306+
}
307+
308+
@Override
309+
public String idFromBaseType() {
310+
return delegate.idFromBaseType();
311+
}
312+
313+
@Override
314+
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
315+
DeserializationConfig config = (DeserializationConfig) context.getConfig();
316+
JavaType result = delegate.typeFromId(context, id);
317+
String className = result.getRawClass().getName();
318+
if (isTrusted(className)) {
319+
return result;
320+
}
321+
boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null;
322+
if (isExplicitMixin) {
323+
return result;
324+
}
325+
Class<?> rawClass = result.getRawClass();
326+
JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(rawClass, JacksonAnnotation.class);
327+
if (jacksonAnnotation != null) {
328+
return result;
329+
}
330+
throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not trusted. " +
331+
"If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or a custom ObjectMapper. " +
332+
"If the serialization is only done by a trusted source, you can also enable default typing.");
333+
}
334+
335+
private boolean isTrusted(String id) {
336+
return TRUSTED_CLASS_NAMES.contains(id);
337+
}
338+
339+
@Override
340+
public String getDescForKnownTypeIds() {
341+
return delegate.getDescForKnownTypeIds();
342+
}
343+
344+
@Override
345+
public JsonTypeInfo.Id getMechanism() {
346+
return delegate.getMechanism();
347+
}
348+
349+
}
350+
144351
}

spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/AbstractExecutionContextSerializerTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.batch.core.repository.dao;
1717

18+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
1819
import org.junit.Test;
1920
import org.springframework.batch.core.JobParameter;
2021
import org.springframework.batch.core.JobParameters;
@@ -175,6 +176,7 @@ protected Map<String, Object> serializationRoundTrip(Map<String, Object> m1) thr
175176

176177
protected abstract ExecutionContextSerializer getSerializer();
177178

179+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
178180
public static class ComplexObject implements Serializable {
179181
private static final long serialVersionUID = 1L;
180182
private String name;

0 commit comments

Comments
 (0)