Unable to deserialize ExecutionContext: the class with [... Class] and name of [... Class] is not trusted

Viewed 4824

I'm working with SpringBoot 2.4.2 and SpringBatch. Deserialization fails with the following stacktrace:

2021-03-29 15:51:28.529 ERROR 30308 --- [           main] o.s.boot.SpringApplication               : Application run failed
java.lang.IllegalArgumentException: Unable to deserialize the execution context
    at org.springframework.batch.core.repository.dao.JdbcExecutionContextDao$ExecutionContextRowMapper.mapRow(JdbcExecutionContextDao.java:328) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.repository.dao.JdbcExecutionContextDao$ExecutionContextRowMapper.mapRow(JdbcExecutionContextDao.java:312) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:94) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:61) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:723) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:713) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:744) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:757) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:815) ~[spring-jdbc-5.3.3.jar:5.3.3]
    at org.springframework.batch.core.repository.dao.JdbcExecutionContextDao.getExecutionContext(JdbcExecutionContextDao.java:129) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.explore.support.SimpleJobExplorer.getStepExecutionDependencies(SimpleJobExplorer.java:238) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.explore.support.SimpleJobExplorer.findRunningJobExecutions(SimpleJobExplorer.java:118) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.3.jar:5.3.3]
    at com.sun.proxy.$Proxy222.findRunningJobExecutions(Unknown Source) ~[na:na]
    at com.xyz.onApplicationEvent(HandleJobsOnAppReady.java:50) ~[classes/:na]
    at com.xyz.onApplicationEvent(HandleJobsOnAppReady.java:24) ~[classes/:na]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) ~[spring-context-5.3.3.jar:5.3.3]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) ~[spring-context-5.3.3.jar:5.3.3]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) ~[spring-context-5.3.3.jar:5.3.3]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:426) ~[spring-context-5.3.3.jar:5.3.3]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:383) ~[spring-context-5.3.3.jar:5.3.3]
    at org.springframework.boot.context.event.EventPublishingRunListener.running(EventPublishingRunListener.java:111) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplicationRunListeners.lambda$running$6(SpringApplicationRunListeners.java:79) ~[spring-boot-2.4.2.jar:2.4.2]
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) ~[na:na]
    at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:117) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:111) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplicationRunListeners.running(SpringApplicationRunListeners.java:79) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:341) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.4.2.jar:2.4.2]
    at com.xyz.Application.main(XYZApplication.java:18) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:578) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: com.fasterxml.jackson.databind.JsonMappingException: The class with [xyz.Class; and name of [xyz.Class; is not trusted. If you believe this class is safe to deserialize, you can add it to the base set of trusted classes at construction time or provide an explicit mapping using Jackson annotations or a custom ObjectMapper. If the serialization is only done by a trusted source, you can also enable default typing. (through reference chain: java.util.HashMap["partition"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:397) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:356) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase.wrapAndThrow(ContainerDeserializerBase.java:181) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer._readAndBindStringKeyMap(MapDeserializer.java:552) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:377) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:29) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:132) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:99) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserializeWithType(MapDeserializer.java:413) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4526) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3513) ~[jackson-databind-2.11.4.jar:2.11.4]
    at org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer.deserialize(Jackson2ExecutionContextStringSerializer.java:133) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer.deserialize(Jackson2ExecutionContextStringSerializer.java:104) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.repository.dao.JdbcExecutionContextDao$ExecutionContextRowMapper.mapRow(JdbcExecutionContextDao.java:325) ~[spring-batch-core-4.3.1.jar:4.3.1]
    ... 46 common frames omitted
Caused by: java.lang.IllegalArgumentException: The class with [xyz.Class; and name of [xyz.Class; is not trusted. If you believe this class is safe to deserialize, you can add it to the base set of trusted classes at construction time or provide an explicit mapping using Jackson annotations or a custom ObjectMapper. If the serialization is only done by a trusted source, you can also enable default typing.
    at org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer$TrustedTypeIdResolver.typeFromId(Jackson2ExecutionContextStringSerializer.java:349) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:154) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:97) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromAny(AsPropertyTypeDeserializer.java:193) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:710) ~[jackson-databind-2.11.4.jar:2.11.4]
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer._readAndBindStringKeyMap(MapDeserializer.java:542) ~[jackson-databind-2.11.4.jar:2.11.4]
    ... 57 common frames omitted

The error is pretty clear and I read a lot about it. However I am not able to solve it.

I added a bean of the type ExecutionContextSerializer to the BatchConfiguration but it does not seem to work:

@Bean
public ExecutionContextSerializer customSerializer(){
    return new Jackson2ExecutionContextStringSerializer(ClassToBeTrusted.class.getName());
}


@Bean
public JobRepository createJobRepository(DataSource dataSource,ExecutionContextSerializer executionContextSerializer) throws Exception {
    JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
    factory.setDataSource(dataSource);
    factory.setTransactionManager(getTransactionManager());
    factory.setSerializer(executionContextSerializer);
    factory.afterPropertiesSet();
    return factory.getObject();
}

The error occurs when:

@Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {

        Set<JobExecution> runningJobExecutions = jobExplorer.findRunningJobExecutions(job.getName());

is executed.

Update: the BatchConfiguration class implements the DefaultBatchConfigurer class now and overrides getJobRepository and getJobExplorer (not sure if this is necessary?) as followed:

    @Override
    public JobRepository getJobRepository() {
        try {
            JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
            factory.setDataSource(datasource);
            factory.setTransactionManager(getTransactionManager());
            factory.setSerializer(customSerializer());
            factory.afterPropertiesSet();
            return factory.getObject();
        }

        catch(Exception e){
            throw new BatchConfigurationException(e);
        }
    }

    @Override
    public JobExplorer getJobExplorer(){
        try{
            JobExplorerFactoryBean factory = new JobExplorerFactoryBean();
            factory.setSerializer(customSerializer());
            factory.setDataSource(datasource);
            factory.afterPropertiesSet();
            return factory.getObject();
        }
        catch(Exception e ){
            throw new BatchConfigurationException(e);
        }
    }

Unfortunately I still see the deserialization error: Myclass is not trusted ... as stated above.

3 Answers

According to Christian's answer, here is how you override default serializer.

package com.my.path.to.config;

import com.my.path.to.entity.MyEntity;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.explore.support.JobExplorerFactoryBean;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class BatchConfiguration {

    @Bean
    public ExecutionContextSerializer customSerializer() {
        return new Jackson2ExecutionContextStringSerializer(MyEntity.class.getName());
    }

    @Bean
    BatchConfigurer myBatchConfigurer(@Qualifier("batchDataSource") DataSource dataSource,
                                      ExecutionContextSerializer executionContextSerializer,
                                      PlatformTransactionManager transactionManager) {
        return new DefaultBatchConfigurer(dataSource) {
            @Override
            protected JobExplorer createJobExplorer() throws Exception {
                JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
                jobExplorerFactoryBean.setDataSource(dataSource);
                jobExplorerFactoryBean
                                .setSerializer(executionContextSerializer);
                jobExplorerFactoryBean.afterPropertiesSet();
                return jobExplorerFactoryBean.getObject();
            }

            @Override
            protected JobRepository createJobRepository() throws Exception {
                JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
                jobRepositoryFactoryBean.setDataSource(dataSource);
                jobRepositoryFactoryBean
                                .setSerializer(executionContextSerializer);
                jobRepositoryFactoryBean.setTransactionManager(transactionManager);
                jobRepositoryFactoryBean.afterPropertiesSet();
                return jobRepositoryFactoryBean.getObject();
            }
        };
    }
}

In order to register a custom JobRepository, you need to provide a Batchconfigurer and override getJobRepository. This is explained in the Configuring and Running a Job section:

You can customize any of these beans by creating a custom implementation
of the BatchConfigurer interface. Typically, extending the DefaultBatchConfigurer 
(which is provided if a BatchConfigurer is not found) and overriding
the required getter is sufficient.

In your case, you are declaring a bean of type JobRepository and expecting Spring Batch to take it into account, this is not enough. You need to make your batch configuration class implement BatchConfigurer or extend DefaultBatchConfigurer.

I could solve the problem by debugging the Jackson2ExecutionContextStringSerializer and its isTrusted logic. In the end I had to add the typed array and the class to the list of trusted classes like this:

public ExecutionContextSerializer customSerializer() {
    return new Jackson2ExecutionContextStringSerializer(new MyClass[0].getClass().getName(), MyClass.class.getName());
}

I became aware of the problem when I saw that [L<fullyQualifiedClassName> was compared with fullyQualifiedClassName

Related