Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Batch Serialization Problems with Java 8 time package

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?

like image 220
Bert S. Avatar asked Oct 27 '15 13:10

Bert S.


2 Answers

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.

like image 50
Michael Minella Avatar answered Oct 22 '22 21:10

Michael Minella


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>
like image 29
KermarrecSeb Avatar answered Oct 22 '22 19:10

KermarrecSeb