Spring Batch Admin in Spring Boot 2

Viktor Reinok
3 min readMay 9, 2022

This article is about how to adapt Simple UI that is Spring Batch Admin for your Spring Boot 2 application.

Spring batch is a great framework for batch processing.
It provides a lot of control in terms of:

Chunk management, transactions, flow control, resilience, IO

Unfortunately, it does not provide an out-of-the-box interface for an easily accessible overview.
There the current solution is Spring Data Flow, which is designed to do a bit more than just that, providing easy to access UI for scheduled jobs.

— — —

Time required: ~15 minutes (optimistic). Most probably an hour due to errors. (please leave feedback in the comments)

Let's go!

Add config

@EnableWebMvc

Excluding config

SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)

Some more config - Properties

# false # disable batch jobs auto-start
spring.batch.job.enabled=false
spring.jmx=true
spring.jmx.enabled=true

# Required for sprint batch admin
spring.main.allow-bean-definition-overriding=true

Additional dependencies

Add the following to your Maven pom file.

<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-admin-manager</artifactId>
<version>1.3.1.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<artifactId>jackson-mapper-asl</artifactId>
<groupId>org.codehaus.jackson</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>

New tables

<databaseChangeLog ...>    <changeSet author="viktor.reinok" id="spring-batch-table">        <sqlFile path="liquibase/4.0.0/schema-postgresql.sql"
relativeToChangelogFile="false"
splitStatements="true"
dbms="postgresql"
stripComments="true"/>
<rollback> <sqlFile path="liquibase/4.0.0/schema-drop-postgresql.sql"
relativeToChangelogFile="false"
splitStatements="true"
dbms="postgresql"
stripComments="true"/>
</rollback>
</changeSet>
</databaseChangeLog>

Here, in case you have your batch dependencies in the same maven module you could ref to the .sql in the build folder.

Actual config

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- README:
This file is a duplication of "classpath:/org/springframework/batch/admin/web/resources/webapp-config.xml" file
in which we do not import "classpath*:/META-INF/spring/batch/bootstrap/manager/data-source-context.xml" file
in order to create and manage datasource using Spring Boot default behavior (and not using Spring Batch Admin Manager)
-->

<!-- Workaround for INT-1831 (You might need it) -->
<!-- <bean id="dummy123" class="java.util.Date"/>-->

<import resource="classpath*:/META-INF/spring/batch/bootstrap/integration/*.xml"/>
<import resource="classpath*:/META-INF/spring/batch/bootstrap/resources/*.xml"/>

<import resource="classpath*:/META-INF/spring/batch/bootstrap/manager/execution-context.xml"/>
<!-- <import resource="classpath*:/META-INF/spring/batch/bootstrap/manager/env-context.xml" />-->
<!-- <import resource="classpath*:/META-INF/spring/batch/bootstrap/manager/jmx-context.xml" />-->

</beans>
import javax.management.MBeanServer;
import javax.sql.DataSource;

import org.springframework.batch.admin.jmx.BatchMBeanExporter;
import org.springframework.batch.admin.service.JobService;
import org.springframework.batch.admin.service.SimpleJobServiceFactoryBean;
import org.springframework.batch.admin.web.filter.ParameterUnpackerFilter;
import org.springframework.batch.core.JobParametersIncrementer;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.support.MapJobRegistry;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

@Configuration
// Do not add @EnableBatchProcessing because there will be a conflict with
// the beans that are already declared in Spring Batch Admin XML webapp config
@ImportResource({"classpath:batch-admin-webapp-config-override.xml"})
@EnableMBeanExport
public class BatchAdminConfiguration {

@Autowired
private PlatformTransactionManager transactionManager;

@Autowired
private MBeanServer mbeanServer;

@Autowired
private JobService jobService;

@Autowired
private JobRepository jobRepository;

@Autowired
DataSource dataSource;

@Bean
public ServletRegistrationBean adminServletRegistrationBean() {
XmlWebApplicationContext applicationContext = new XmlWebApplicationContext();
applicationContext.setConfigLocation("classpath:/org/springframework/batch/admin/web/resources/servlet-config.xml");

DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
dispatcherServlet.setContextConfigLocation(null);

ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/*");
servletRegistrationBean.setName("batch-admin");

return servletRegistrationBean;
}

@Bean
public ParameterUnpackerFilter parameterUnpackerFilter() {
ParameterUnpackerFilter filter = new ParameterUnpackerFilter();
filter.setPrefix("unpack_");
filter.setPutEmptyParamsInPath(true);
return filter;
}

@Bean
@Qualifier("jobService")
public JobService jobService() throws Exception {
SimpleJobServiceFactoryBean bean = new SimpleJobServiceFactoryBean();
bean.setJobRepository(jobRepository);
bean.setJobLocator(new MapJobRegistry());
bean.setJobLauncher(new SimpleJobLauncher());
bean.setDataSource(dataSource);
bean.afterPropertiesSet();

return bean.getObject();
}

@Bean
@DependsOn("jobService")
@Scope( proxyMode = ScopedProxyMode.TARGET_CLASS )
public BatchMBeanExporter batchMBeanExporter() {
BatchMBeanExporter batchMBeanExporter = new BatchMBeanExporter();
batchMBeanExporter.setDefaultDomain("spring.application");
batchMBeanExporter.setServer(mbeanServer);
batchMBeanExporter.setJobService(jobService);
return batchMBeanExporter;
}

@Bean
public JobBuilderFactory jobBuilderFactory() {
return new JobBuilderFactory(jobRepository);
}

@Bean
public StepBuilderFactory stepBuilderFactory() {
return new StepBuilderFactory(jobRepository, transactionManager);
}

@Bean
public JobParametersIncrementer jobParametersIncrementer() {
return new RunIdIncrementer();
}

}

A hack (overwrite Spring class)

Add following class to org.springframework.web.util;

We are coping a static method from Spring Web 4 module and adding it to the latest Spring boot class.

https://gist.github.com/vikreinok/770dadd35e73df9bd0df6a7f254ff3a6

Final word

Let me know if you are facing some error on the way, lets's try to resolve them together. Leave a comment below or write me ;)

References

  1. https://spring.io/projects/spring-batch
  2. https://spring.io/projects/spring-cloud-dataflow
  3. https://codenotfound.com/spring-batch-admin-example.html
  4. https://stackoverflow.com/questions/48788459/not-able-to-run-springboot-1-5-10-release-on-local-tomcatspring-boot-embedded

--

--