Spring Batch Serialization Problems with Java 8 time package
Asked Answered
P

2

10

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?

Pathogenesis answered 27/10, 2015 at 13:51 Comment(1)
have you add @EnableBatchProcessing annotation on a configuration class ?Condensation
T
4

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>
Tashinatashkent answered 17/10, 2017 at 15:12 Comment(0)
V
3

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.

Vasoconstrictor answered 4/11, 2015 at 19:9 Comment(7)
I have added my own customer serializer. However, the JobRepositoryFactoryBean is still using the default XStreamExecutionContextStringSerializer, which is leading to exceptions like this when I try to read job data from the JobExplorer:Pathogenesis
Caused by: com.thoughtworks.xstream.converters.ConversionException: Cannot deserialize object with new readObject()/writeObject() methods ---- Debugging information ---- class : java.time.LocalDate required-type : java.time.LocalDate converter-type : com.thoughtworks.xstream.converters.reflection.SerializableConverterPathogenesis
It also appears that hitting the restart button on the Spring Batch Admin UI triggers JobExecutionController.restart, which is trying to serialize with the default XStreamExecutionContextStringSerializer. I get the same exceptions in that scenario (and my stack trace includes XStreamExecutionContextStringSerializer rather than my custom serializer).Pathogenesis
Can you please post the configuration for your JobRepositoryFactoryBean in your question?Vasoconstrictor
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean" p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" p:tablePrefix="SPG.BATCH_"> <property name="serializer"> <bean class="org.my.util.BetterXStreamExecutionContextStringSerializer"/> </property> </bean>Pathogenesis
Correction: I actually have a parent jobRepository shared with other jobs in my application. I have overridden my particular one with the serializer described above.Pathogenesis
Can you please update your question with the configuration? Also, the rest of the configuration would be helpful (are you using boot, etc)?Vasoconstrictor

© 2022 - 2024 — McMap. All rights reserved.