BATCH-2680: add custom Jackson module to (de)serialize job parameters

Currently, the Jackson2ExecutionContextStringSerializer fails to
deserialize json representations of:

* empty JobParameters instances due to the presence of "empty":true
 in the serialized json String. In this case, Jackson is not able to find a
 property named "empty" in the target type (JobParameters)

* JobParameter instances due to the presence of several constructors.
In this case, Jackson does not know which constructor to use.

This commit fixes these two issues by adding a custom Jackson module with:

* a mixin to ignore the "isEmpty" getter in JobParameters type
* a custom deserializer for JobParameter type

Resolves BATCH-2680
This commit is contained in:
Mahmoud Ben Hassine
2018-02-14 11:09:52 +01:00
committed by Michael Minella
parent 867fa5e2af
commit b0ffe55113
2 changed files with 149 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2016 the original author or authors.
* Copyright 2008-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,14 +18,23 @@ package org.springframework.batch.core.repository.dao;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.util.Assert;
@@ -33,6 +42,7 @@ import org.springframework.util.Assert;
* Implementation that uses Jackson2 to provide (de)serialization.
*
* @author Marten Deinum
* @author Mahmoud Ben Hassine
* @since 3.0.7
*
* @see ExecutionContextSerializer
@@ -46,6 +56,7 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex
this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
this.objectMapper.enableDefaultTyping();
this.objectMapper.registerModule(new JobParametersModule());
}
public void setObjectMapper(ObjectMapper objectMapper) {
@@ -66,4 +77,64 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex
objectMapper.writeValue(out, context);
}
// BATCH-2680
/**
* Custom Jackson module to support {@link JobParameter} and {@link JobParameters}
* deserialization.
*/
private class JobParametersModule extends SimpleModule {
private JobParametersModule() {
super("Job parameters module");
setMixInAnnotation(JobParameters.class, JobParametersMixIn.class);
addDeserializer(JobParameter.class, new JobParameterDeserializer());
}
private abstract class JobParametersMixIn {
@JsonIgnore
abstract boolean isEmpty();
}
private class JobParameterDeserializer extends StdDeserializer<JobParameter> {
private static final String IDENTIFYING_KEY_NAME = "identifying";
private static final String TYPE_KEY_NAME = "type";
private static final String VALUE_KEY_NAME = "value";
JobParameterDeserializer() {
super(JobParameter.class);
}
@Override
public JobParameter deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.readValueAsTree();
boolean identifying = node.get(IDENTIFYING_KEY_NAME).asBoolean();
String type = node.get(TYPE_KEY_NAME).asText();
JsonNode value = node.get(VALUE_KEY_NAME);
Object parameterValue;
switch (JobParameter.ParameterType.valueOf(type)) {
case STRING: {
parameterValue = value.asText();
return new JobParameter((String) parameterValue, identifying);
}
case DATE: {
parameterValue = new Date(value.get(1).asLong());
return new JobParameter((Date) parameterValue, identifying);
}
case LONG: {
parameterValue = value.get(1).asLong();
return new JobParameter((Long) parameterValue, identifying);
}
case DOUBLE: {
parameterValue = value.asDouble();
return new JobParameter((Double) parameterValue, identifying);
}
}
return null;
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.batch.core.repository.dao;
import org.junit.Test;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import java.io.*;
@@ -34,6 +36,7 @@ import static org.hamcrest.Matchers.hasEntry;
* @author Thomas Risberg
* @author Michael Minella
* @author Marten Deinum
* @author Mahmoud Ben Hassine
*/
public abstract class AbstractExecutionContextSerializerTests {
@@ -51,6 +54,79 @@ public abstract class AbstractExecutionContextSerializerTests {
compareContexts(m1, m2);
}
@Test
public void testSerializeStringJobParameter() throws Exception {
Map<String, Object> m1 = new HashMap<>();
m1.put("name", new JobParameter("foo"));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeDateJobParameter() throws Exception {
Map<String, Object> m1 = new HashMap<>();
m1.put("birthDate", new JobParameter(new Date(123456790123L)));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeDoubleJobParameter() throws Exception {
Map<String, Object> m1 = new HashMap<>();
m1.put("weight", new JobParameter(80.5D));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeLongJobParameter() throws Exception {
Map<String, Object> m1 = new HashMap<>();
m1.put("age", new JobParameter(20L));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeNonIdentifyingJobParameter() throws Exception {
Map<String, Object> m1 = new HashMap<>();
m1.put("name", new JobParameter("foo", false));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeJobParameters() throws Exception {
Map<String, JobParameter> jobParametersMap = new HashMap<>();
jobParametersMap.put("paramName", new JobParameter("paramValue"));
Map<String, Object> m1 = new HashMap<>();
m1.put("params", new JobParameters(jobParametersMap));
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testSerializeEmptyJobParameters() throws IOException {
Map<String, Object> m1 = new HashMap<>();
m1.put("params", new JobParameters());
Map<String, Object> m2 = serializationRoundTrip(m1);
compareContexts(m1, m2);
}
@Test
public void testComplexObject() throws Exception {
Map<String, Object> m1 = new HashMap<String, Object>();