Reputation: 155
I have a Spring-batch application that stores several Java 8 time objects in the JobExecutionContext. I am using the default serializer for my JobRespository. I am facing exceptions when parsing back out the data that is being written into the BATCH_STEP_EXECUTION_CONTEXT table. I have a LocalDateTime that is being stored as:
{
"@resolves-to": "java.time.Ser",
"byte": [5,
8,
18,
8,
45,
50],
"int": [2015,
10000000]
}
This leads to an exception when I try to read from the previous JobExecution data:
Caused by: java.lang.ClassCastException: java.lang.Byte cannot be cast to java.lang.Integer
at com.thoughtworks.xstream.core.util.CustomObjectInputStream.readInt(CustomObjectInputStream.java:144) ~[xstream-1.4.8.jar:1.4.8]
at java.time.LocalDate.readExternal(LocalDate.java:2070) ~[na:1.8.0_45]
at java.time.LocalDateTime.readExternal(LocalDateTime.java:2002) ~[na:1.8.0_45]
at java.time.Ser.readInternal(Ser.java:259) ~[na:1.8.0_45]
at java.time.Ser.readExternal(Ser.java:246) ~[na:1.8.0_45]
at com.thoughtworks.xstream.converters.reflection.ExternalizableConverter.unmarshal(ExternalizableConverter.java:167) ~[xstream-1.4.8.jar:1.4.8]
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:72) ~[xstream-1.4.8.jar:na]
... 97 common frames omitted
I am using Spring-batch 3.0.5.RELEASE. I've also tried upgrading to the latest versions of xstream (1.4.8) and Jettison (1.3.7), but I get the same exception.
This appears to be a known issue with XStream (link). The suggestion was to register a custom converter within XStream. However, spring-batch does not expose the actual XStream object in order to register a converter. Any suggestions on how to proceed?
Upvotes: 10
Views: 5639
Reputation: 41
I had the same problem while deserializing LocalDate
from step execution context.
So I have to make my proper converter :
public class DateConverter implements Converter {
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
private static final DateTimeFormatter DEFAULT_DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN);
public DateConverter() {
super();
}
public boolean canConvert(Class clazz) {
return LocalDate.class.isAssignableFrom(clazz);
}
/**
* Convert LocalDate to String
*/
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
LocalDate date = (LocalDate) value;
String result = date.format(DEFAULT_DATE_FORMATTER);
writer.setValue(result);
}
/**
* convert Xml to LocalDate
*/
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
LocalDate result = LocalDate.parse(reader.getValue(), DEFAULT_DATE_FORMATTER);
return result;
}
}
After that i have to create my proper XStreamExecutionContextStringSerializer
for using my converter
/**
* My XStreamExecutionContextStringSerializer
* @since 1.0
*/
public class MyXStreamExecutionContextStringSerializer implements ExecutionContextSerializer, InitializingBean {
private ReflectionProvider reflectionProvider = null;
private HierarchicalStreamDriver hierarchicalStreamDriver;
private XStream xstream;
public void setReflectionProvider(ReflectionProvider reflectionProvider) {
this.reflectionProvider = reflectionProvider;
}
public void setHierarchicalStreamDriver(HierarchicalStreamDriver hierarchicalStreamDriver) {
this.hierarchicalStreamDriver = hierarchicalStreamDriver;
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
public synchronized void init() throws Exception {
if (hierarchicalStreamDriver == null) {
this.hierarchicalStreamDriver = new JettisonMappedXmlDriver();
}
if (reflectionProvider == null) {
xstream = new XStream(hierarchicalStreamDriver);
}
else {
xstream = new XStream(reflectionProvider, hierarchicalStreamDriver);
}
// Convert LocalDate
xstream.registerConverter(new DateConverter());
}
/**
* Serializes the passed execution context to the supplied OutputStream.
*
* @param context
* @param out
* @see Serializer#serialize(Object, OutputStream)
*/
@Override
public void serialize(Map<String, Object> context, OutputStream out) throws IOException {
Assert.notNull(context);
Assert.notNull(out);
out.write(xstream.toXML(context).getBytes());
}
/**
* Deserializes the supplied input stream into a new execution context.
*
* @param in
* @return a reconstructed execution context
* @see Deserializer#deserialize(InputStream)
*/
@SuppressWarnings("unchecked")
@Override
public Map<String, Object> deserialize(InputStream in) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(in));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return (Map<String, Object>) xstream.fromXML(sb.toString());
}
}
The last step is to register MyXStreamExecutionContextStringSerializer in the file execution-context.xml that register the bean jobRepository
<bean id="jobRepository"
class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
<property name="tablePrefix" value="${batch.table.prefix:BATCH.BATCH_}" />
<property name="serializer"> <bean class="com.batch.config.MyXStreamExecutionContextStringSerializer"/> </property>
</bean>
Upvotes: 4
Reputation: 21493
Spring Batch allows you to configure your own serializer for the ExecutionContext
by implementing the ExecutionContextSerializer
interface and injecting it into the JobRepositoryFactoryBean
.
You are correct in that we don't allow you to inject your own XStream instance currently (although it seems like a reasonable extension point given this issue). For now, you'd have to either extend or copy XStreamExecutionContextStringSerializer
and use your own XStream instance.
Upvotes: 3