Spring Batch - How to read from One Table and Write Data into two different table - spring

I'm using Spring Boot and Spring Batch to read data from One table of source database table and split the data and write it into two tables of target database.
I choose to use CompositeItemWriter for this, but CompositeItemWriter<?> only one type. I want to write few fields in one table and other fields into another table.
Say: OLD Customer and NEW Customer.
Error:
The constructor CustomerClassifier(JdbcBatchItemWriter, JdbcBatchItemWriter) is undefined
ClassifierCompositeItemApplication.java
#EnableBatchProcessing
#SpringBootApplication
public class ClassifierCompositeItemApplication {
private JobBuilderFactory jobBuilderFactory;
private StepBuilderFactory stepBuilderFactory;
public ClassifierCompositeItemApplication(JobBuilderFactory jobs, StepBuilderFactory steps) {
this.jobBuilderFactory = jobs;
this.stepBuilderFactory = steps;
}
#Value("classpath:input/customer.csv")
private Resource inputResource;
#Bean
#StepScope
public FlatFileItemReader<Customer> classifierCompositeWriterItemReader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerFileReader")
.resource(inputResource).delimited()
.names(new String[] { "firstName", "middleInitial", "lastName", "address", "city", "state", "zip" })
.targetType(Customer.class)
.build();
}
#Bean
public ClassifierCompositeItemWriter<Customer> compositeItemWriter() throws IOException {
final Classifier<Customer, ItemWriter<? super Customer>> classifier = new CustomerClassifier(
this.customer1(null), this.customer2(null));
return new ClassifierCompositeItemWriterBuilder<Customer>().classifier(classifier).build();
}
#Bean
public JdbcBatchItemWriter<Customer> customer1(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Customer>()
.namedParametersJdbcTemplate(new NamedParameterJdbcTemplate(dataSource))
.sql("INSERT INTO TBL_CUSTOMER_WRITER (firstname, middleinitial, lastname, address, city, " + "state, "
+ "zipcode) " + "VALUES(:firstName, " + ":middleInitial, " + ":lastName, " + ":address, "
+ ":city, " + ":state, " + ":zip)")
.beanMapped().build();
}
#Bean
public JdbcBatchItemWriter<NewCustomer> customer2(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<NewCustomer>()
.namedParametersJdbcTemplate(new NamedParameterJdbcTemplate(dataSource))
.sql("INSERT INTO TBL_CUSTOMER_WRITER (firstname, middleinitial, lastname, address, city, " + "state, "
+ "zipcode) " + "VALUES(:firstName, " + ":middleInitial, " + ":lastName, " + ":address, "
+ ":city, " + ":state, " + ":zip)")
.beanMapped().build();
}
#Bean
public Step classifierCompositeWriterStep() throws IOException {
return this.stepBuilderFactory.get("compositeWriterStep")
.<Customer, Customer>chunk(10)
.reader(this.classifierCompositeWriterItemReader())
.writer(this.compositeItemWriter())
.stream(this.customer1(null))
.stream(this.customer2(null))
.build();
}
#Bean
public Job classifierCompositeWriterJob() throws IOException {
return this.jobBuilderFactory.get("compositeWriterJob").start(this.classifierCompositeWriterStep()).build();
}
public static void main(String[] args) {
SpringApplication.run(ClassifierCompositeItemApplication.class, args);
}
}
CustomerClassifier.java
#AllArgsConstructor
public class CustomerClassifier implements Classifier<Customer, ItemWriter<? super Customer>> {
private static final long serialVersionUID = 1L;
private final ItemWriter<Customer> customer1;
private final ItemWriter<Customer> customer2;
#Override
public ItemWriter<? super Customer> classify(Customer customer) {
if (customer.getState().matches("^[A-M].*")) {
return customer1;
} else {
return customer2;
}
}
}

You can use a ClassifierCompositeItemWriter. This composite writer is designed to classify items and call a delegate item writer for each class.
So in your case, you can have am item writer for old customers and another one for new customers and warp them in a classifier item writer. You can use one of the Classifier implementations provided by Spring or create a custom one (for example an item processor cam flag your items as old/new and the classifier uses this flag to classify them).
EDIT: Add an example:
import java.util.Arrays;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ClassifierCompositeItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.item.support.builder.ClassifierCompositeItemWriterBuilder;
import org.springframework.classify.Classifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
#EnableBatchProcessing
public class MyJob {
#Bean
public ItemReader<Customer> itemReader() {
return new ListItemReader<>(Arrays.asList(new Customer("foo"), new Customer("bar")));
}
#Bean
public ItemWriter<Customer> fooWriter() {
return items -> {
for (Customer item : items) {
System.out.println("foo writer: item " + item.name);
}
};
}
#Bean
public ItemWriter<Customer> barWriter() {
return items -> {
for (Customer item : items) {
System.out.println("bar writer: item " + item.name);
}
};
}
#Bean
public ClassifierCompositeItemWriter<Customer> classifierCompositeItemWriter() {
final Classifier<Customer, ItemWriter<? super Customer>> classifier =
new CustomerClassifier(this.fooWriter(), this.barWriter());
return new ClassifierCompositeItemWriterBuilder<Customer>()
.classifier(classifier)
.build();
}
#Bean
public Job job(JobBuilderFactory jobs, StepBuilderFactory steps) {
return jobs.get("job")
.start(steps.get("step")
.<Customer, Customer>chunk(5)
.reader(itemReader())
.writer(classifierCompositeItemWriter())
.build())
.build();
}
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(MyJob.class);
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
jobLauncher.run(job, new JobParameters());
}
static class Customer {
String name;
public Customer(String name) {
this.name = name;
}
}
static class CustomerClassifier implements Classifier<Customer, ItemWriter<? super Customer>> {
private ItemWriter<? super Customer> fooItemWriter;
private ItemWriter<? super Customer> barItemWriter;
public CustomerClassifier(ItemWriter<? super Customer> fooItemWriter, ItemWriter<? super Customer> barItemWriter) {
this.fooItemWriter = fooItemWriter;
this.barItemWriter = barItemWriter;
}
#Override
public ItemWriter<? super Customer> classify(Customer customer) {
return customer.name.startsWith("f") ? fooItemWriter : barItemWriter;
}
}
}
This prints:
foo writer: item foo
bar writer: item bar

Related

How to use ClassifierCompositeItemProcessor in Spring Batch and write data into same table for Insert and Upsert?

I went through the link - https://github.com/spring-projects/spring-batch/blob/master/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ClassifierCompositeItemProcessorTests.java, but did not strike much out of it.
I am trying to replace ETL Informatica mapping logic into the Batch. I am looking to separate out Status=I and Status=U into separate (Individual) processor and then further perform lookup and massage the data and then write those records directly into the table for Status=I and for status=U, perform another complex logic (like lookups, massaging and match and merge logic) and then upsert those records again into the same table.
I've tried to do POC, where I am looking to segregate the records in the processor
CustomerClassifier.java
public class CustomerClassifier implements Classifier<Customer, ItemProcessor<Customer, Customer>> {
private ItemProcessor<Customer, Customer> insertCustomerProcessor;
private ItemProcessor<Customer, Customer> updateCustomerProcessor;
public CustomerClassifier(ItemProcessor<Customer, Customer> evenCustomerProcessor, ItemProcessor<Customer, Customer> oddCustomerProcessor) {
this.insertCustomerProcessor= insertCustomerProcessor;
this.updateCustomerProcessor= updateCustomerProcessor;
}
#Override
public ItemProcessor<Customer, Customer> classify(Customer customer) {
return customer.getStatus().equals("I") ? insertCustomerProcessor : updateCustomerProcessor;
}
}
OddCustomerProcessor.java
public class OddCustomerProcessor implements ItemProcessor<Customer, Customer> {
#Override
public Customer process(Customer item) throws Exception {
Customer customer = new Customer();
// Perform some msaaging and lookups here
customer.setId(item.getId());
customer.setFirstName(item.getFirstName());
customer.setLastName(item.getLastName());
customer.setBirthdate(item.getBirthdate());
customer.setStatus(item.getStatus());
return customer;
}
}
EvenCustomerProcessor.java
public class EvenCustomerProcessor implements ItemProcessor<Customer, Customer> {
#Override
public Customer process(Customer item) throws Exception {
Customer customer = new Customer();
// Perform some msaaging and lookups here
customer.setId(item.getId());
customer.setFirstName(item.getFirstName());
customer.setLastName(item.getLastName());
customer.setBirthdate(item.getBirthdate());
customer.setStatus(item.getStatus());
return customer;
}
}
CustomLineAggregator.java
public class CustomLineAggregator implements LineAggregator<Customer> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String aggregate(Customer item) {
try {
return objectMapper.writeValueAsString(item);
} catch (Exception e) {
throw new RuntimeException("Unable to serialize Customer", e);
}
}
}
Customer.java
#Data
#AllArgsConstructor
#Builder
#NoArgsConstructor
public class Customer {
private Long id;
private String firstName;
private String lastName;
private String birthdate;
private String status;
}
Error-
The method setClassifier(Classifier<? super Customer,ItemProcessor<?,? extends Customer>>) in the type ClassifierCompositeItemProcessor<Customer,Customer> is not applicable for the
arguments (CustomerClassifier)
Configuration
#Configuration
public class JobConfiguration {
#Autowired
private JobBuilderFactory jobBuilderFactory;
#Autowired
private StepBuilderFactory stepBuilderFactory;
#Autowired
private DataSource dataSource;
#Bean
public JdbcPagingItemReader<Customer> customerPagingItemReader(){
// reading database records using JDBC in a paging fashion
JdbcPagingItemReader<Customer> reader = new JdbcPagingItemReader<>();
reader.setDataSource(this.dataSource);
reader.setFetchSize(1000);
reader.setRowMapper(new CustomerRowMapper());
// Sort Keys
Map<String, Order> sortKeys = new HashMap<>();
sortKeys.put("id", Order.ASCENDING);
// MySQL implementation of a PagingQueryProvider using database specific features.
MySqlPagingQueryProvider queryProvider = new MySqlPagingQueryProvider();
queryProvider.setSelectClause("id, firstName, lastName, birthdate");
queryProvider.setFromClause("from customer");
queryProvider.setSortKeys(sortKeys);
reader.setQueryProvider(queryProvider);
return reader;
}
#Bean
public EvenCustomerProcessor evenCustomerProcessor() {
return new EvenCustomerProcessor();
}
#Bean
public OddCustomerProcessor oddCustomerProcessor() {
return new OddCustomerProcessor();
}
#Bean
public JdbcBatchItemWriter<Customer> customerItemWriter(){
JdbcBatchItemWriter<Customer> batchItemWriter = new JdbcBatchItemWriter<>();
batchItemWriter.setDataSource(dataSource);
batchItemWriter.setSql(""); // Query Goes here
return batchItemWriter;
}
#Bean
public ClassifierCompositeItemProcessor<Customer, Customer> classifierCustomerCompositeItemProcessor() throws Exception{
ClassifierCompositeItemProcessor<Customer, Customer> itemProcessor = new ClassifierCompositeItemProcessor<>();
itemProcessor.setClassifier(new CustomerClassifier(evenCustomerProcessor(), oddCustomerProcessor()));
}
#Bean
public Step step1() throws Exception {
return stepBuilderFactory.get("step1")
.<Customer, Customer> chunk(10)
.reader(customerPagingItemReader())
.processor(classifierCustomerCompositeItemProcessor())
.writer(customerItemWriter())
.build();
}
#Bean
public Job job() throws Exception {
return jobBuilderFactory.get("job")
.start(step1())
.build();
}
}
You can remove the CustomerClassifier and define the composite item processor as follows:
#Bean
public ClassifierCompositeItemProcessor<Customer, Customer> classifierCustomerCompositeItemProcessor(
EvenCustomerProcessor evenCustomerProcessor,
OddCustomerProcessor oddCustomerProcessor
) {
ClassifierCompositeItemProcessor<Customer, Customer> itemProcessor = new ClassifierCompositeItemProcessor<>();
itemProcessor.setClassifier(new Classifier<Customer, ItemProcessor<?, ? extends Customer>>() {
#Override
public ItemProcessor<?, ? extends Customer> classify(Customer customer) {
return customer.getStatus().equals("I") ? evenCustomerProcessor : oddCustomerProcessor;
}
});
return itemProcessor;
}
Then update your step definition as follows:
#Bean
public Step step1() throws Exception {
return stepBuilderFactory.get("step1")
.<Customer, Customer> chunk(10)
.reader(customerPagingItemReader())
.processor(classifierCustomerCompositeItemProcessor(evenCustomerProcessor(), oddCustomerProcessor()))
.writer(customerItemWriter())
.build();
}

Records are not written in files when invoked from BillerOrderWriter which implements ItemWriter in Spring Batch

I am trying to write successful records using one writer and failed records in another writer.
I have written BillerOrderWriter class which implements ItemWriter. I put some log statements and I can see it writes success billerOrderId or failed billerOrderId . But, it seems like it does not invoke DatabaseToCsvFileJobConfig or SuccessfulOrdersToCsvFileJobConfig .
public class BillerOrderWriter implements ItemWriter<BillerOrder>{
private static Logger log = LoggerFactory.getLogger("BillerOrderWriter.class");
#Autowired
SuccessfulOrdersToCsvFileJobConfig successfulOrdersToCsvFileJobConfig;
#Autowired
DatabaseToCsvFileJobConfig databaseToCsvFileJobConfig;
#Override
public void write(List<? extends BillerOrder> items) throws Exception {
for (BillerOrder item : items) {
log.info("item = " + item.toString());
if (item.getResult().equals("SUCCESS")) {
log.info(" Success billerOrderId = " + item.getBillerOrderId());
successfulOrdersToCsvFileJobConfig.successfulDatabaseCsvItemWriter();
} else {
log.info("Failed billerOrderId = " + item.getBillerOrderId());
databaseToCsvFileJobConfig.databaseCsvItemWriter();
}
}
}
}
Here is BatchConfig class.
#Bean
public BillerOrderWriter billerOrderWriter() {
return new BillerOrderWriter();
}
#Bean
public Job importJobOrder(JobCompletionNotificationListner listener, Step step1) {
return jobBuilderFactory.get("importJobOrder")
.incrementer(new RunIdIncrementer())
.listener(listener)
.flow(step1)
.end()
.build();
}
#Bean(name="step1")
public Step step1(BillerOrderWriter billerOrderWriter) {
return stepBuilderFactory.get("step1")
.<BillerOrder, BillerOrder> chunk(10)
.reader((ItemReader<? extends BillerOrder>) reader())
.processor(processor())
.writer(billerOrderWriter)
.build();
}
Here is my successwriter and failedwriter class .
#Configuration
public class SuccessfulOrdersToCsvFileJobConfig {
private static Logger log = LoggerFactory.getLogger("SuccessfulOrdersToCsvFileJobConfig.class");
#Bean
public ItemWriter<BillerOrder> successfulDatabaseCsvItemWriter() {
log.info("Entering SuccessfulOrdersToCsvFileJobConfig...");
FlatFileItemWriter<BillerOrder> csvFileWriter = new FlatFileItemWriter<>();
String exportFileHeader = "BillerOrderId;SuccessMessage";
OrderWriter headerWriter = new OrderWriter(exportFileHeader);
csvFileWriter.setHeaderCallback(headerWriter);
String exportFilePath = "/tmp/SuccessBillerOrderIdForRetry.csv";
csvFileWriter.setResource(new FileSystemResource(exportFilePath));
LineAggregator<BillerOrder> lineAggregator = createOrderLineAggregator();
csvFileWriter.setLineAggregator(lineAggregator);
return csvFileWriter;
}
private LineAggregator<BillerOrder> createOrderLineAggregator() {
log.info("Entering createOrderLineAggregator...");
DelimitedLineAggregator<BillerOrder> lineAggregator = new DelimitedLineAggregator<>();
lineAggregator.setDelimiter(";");
FieldExtractor<BillerOrder> fieldExtractor = createOrderFieldExtractor();
lineAggregator.setFieldExtractor(fieldExtractor);
return lineAggregator;
}
private FieldExtractor<BillerOrder> createOrderFieldExtractor() {
log.info("Entering createOrderFieldExtractor...");
BeanWrapperFieldExtractor<BillerOrder> extractor = new BeanWrapperFieldExtractor<>();
extractor.setNames(new String[] {"billerOrderId","successMessage"});
return extractor;
}
}
#Configuration
public class DatabaseToCsvFileJobConfig {
private static Logger log = LoggerFactory.getLogger("DatabaseToCsvFileJobConfig.class");
#Bean
public ItemWriter<BillerOrder> databaseCsvItemWriter() {
log.info("Entering databaseCsvItemWriter...");
FlatFileItemWriter<BillerOrder> csvFileWriter = new FlatFileItemWriter<>();
String exportFileHeader = "BillerOrderId;ErrorMessage";
OrderWriter headerWriter = new OrderWriter(exportFileHeader);
csvFileWriter.setHeaderCallback(headerWriter);
String exportFilePath = "/tmp/FailedBillerOrderIdForRetry.csv";
csvFileWriter.setResource(new FileSystemResource(exportFilePath));
LineAggregator<BillerOrder> lineAggregator = createOrderLineAggregator();
csvFileWriter.setLineAggregator(lineAggregator);
return csvFileWriter;
}
private LineAggregator<BillerOrder> createOrderLineAggregator() {
log.info("Entering createOrderLineAggregator...");
DelimitedLineAggregator<BillerOrder> lineAggregator = new DelimitedLineAggregator<>();
lineAggregator.setDelimiter(";");
FieldExtractor<BillerOrder> fieldExtractor = createOrderFieldExtractor();
lineAggregator.setFieldExtractor(fieldExtractor);
return lineAggregator;
}
private FieldExtractor<BillerOrder> createOrderFieldExtractor() {
log.info("Entering createOrderFieldExtractor...");
BeanWrapperFieldExtractor<BillerOrder> extractor = new BeanWrapperFieldExtractor<>();
extractor.setNames(new String[] {"billerOrderId","errorMessage"});
return extractor;
}
}
Here is my job completion listener class.
#Component
public class JobCompletionNotificationListner extends JobExecutionListenerSupport {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(JobCompletionNotificationListner.class);
#Override
public void afterJob(JobExecution jobExecution) {
log.info("In afterJob ...");
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
DatabaseToCsvFileJobConfig databaseToCsvFileJobConfig = new DatabaseToCsvFileJobConfig();
SuccessfulOrdersToCsvFileJobConfig successfulOrdersToCsvFileJobConfig = new SuccessfulOrdersToCsvFileJobConfig();
}
}
}
In your BillerOrderWriter#write method, it is supposed to write code that does the actual write operation of items to a data sink. But in your case, you are calling successfulOrdersToCsvFileJobConfig.successfulDatabaseCsvItemWriter(); and databaseToCsvFileJobConfig.databaseCsvItemWriter(); which create item writer beans. You should inject those delegate writers and call their write method when needed, something like:
public class BillerOrderWriter implements ItemWriter<BillerOrder>{
private ItemWriter<BillerOrder> successfulDatabaseCsvItemWriter;
private ItemWriter<BillerOrder> databaseCsvItemWriter;
// constructor with successfulDatabaseCsvItemWriter + databaseCsvItemWriter
#Override
public void write(List<? extends BillerOrder> items) throws Exception {
for (BillerOrder item : items) {
if (item.getResult().equals("SUCCESS")) {
successfulDatabaseCsvItemWriter.write(Collections.singletonList(item));
} else {
databaseCsvItemWriter.write(Collections.singletonList(item));
}
}
}
}
Instead Of BillerOrderWriter, I wroter BillerOrderClassifier class.
public class BillerOrderClassifier implements Classifier<BillerOrder, ItemWriter<? super BillerOrder>> {
private static final long serialVersionUID = 1L;
private ItemWriter<BillerOrder> successItemWriter;
private ItemWriter<BillerOrder> failedItemWriter;
public BillerOrderClassifier(ItemWriter<BillerOrder> successItemWriter, ItemWriter<BillerOrder> failedItemWriter) {
this.successItemWriter = successItemWriter;
this.failedItemWriter = failedItemWriter;
}
#Override
public ItemWriter<? super BillerOrder> classify(BillerOrder billerOrder) {
return billerOrder.getResult().equals("SUCCESS") ? successItemWriter : failedItemWriter;
}
}
In BatchConfiguration, I wrote classifierBillerOrderCompositeItemWriter method.
#Bean
public ClassifierCompositeItemWriter<BillerOrder> classifierBillerOrderCompositeItemWriter() throws Exception {
ClassifierCompositeItemWriter<BillerOrder> compositeItemWriter = new ClassifierCompositeItemWriter<>();
compositeItemWriter.setClassifier(new BillerOrderClassifier(successfulOrdersToCsvFileJobConfig.successfulDatabaseCsvItemWriter(), databaseToCsvFileJobConfig.databaseCsvItemWriter()));
return compositeItemWriter;
}
#Bean(name="step1")
public Step step1() throws Exception{
return stepBuilderFactory.get("step1")
.<BillerOrder, BillerOrder> chunk(10)
.reader((ItemReader<? extends BillerOrder>) reader())
.processor(processor())
.writer(classifierBillerOrderCompositeItemWriter())
.stream(successfulOrdersToCsvFileJobConfig.successfulDatabaseCsvItemWriter())
.stream(databaseToCsvFileJobConfig.databaseCsvItemWriter())
.build();
}

The method stream(ItemStream) in the type AbstractTaskletStepBuilder<SimpleStepBuilder<Customer,Customer>> is not applicable for the arguments (

How to classify the elements using Spring Batch ? I want to write data into two different tables or files for now doing this in the console.
Error:
The method stream(ItemStream) in the type AbstractTaskletStepBuilder> is not applicable for the arguments (ItemWriter)
#EnableBatchProcessing
#SpringBootApplication
public class ClassifierCompositeItemApplication {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
#Value("classpath:input/customer.csv")
private Resource inputResource;
public ClassifierCompositeItemApplication(JobBuilderFactory jobs, StepBuilderFactory steps) {
this.jobBuilderFactory = jobs;
this.stepBuilderFactory = steps;
}
#Bean
#StepScope
public FlatFileItemReader<Customer> classifierCompositeWriterItemReader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerFileReader")
.resource(inputResource)
.delimited()
.names(new String[] { "firstName", "middleInitial", "lastName", "address", "city", "state", "zip" })
.targetType(Customer.class)
.build();
}
#Bean
public ClassifierCompositeItemWriter<Customer> compositeItemWriter() throws IOException {
final Classifier<Customer, ItemStreamWriter<? super Customer>> classifier = new CustomerClassifier(
this.customer1(), this.customer2());
return new ClassifierCompositeItemWriterBuilder<Customer>()
.classifier(classifier)
.build();
}
#Bean
#StepScope
public ItemStreamWriter<Customer> customer1() throws IOException {
System.out.println("Customer #1");
return new ItemStreamWriter<Customer>() {
#Override
public void open(ExecutionContext executionContext) throws ItemStreamException {
}
#Override
public void update(ExecutionContext executionContext) throws ItemStreamException {
}
#Override
public void close() throws ItemStreamException {
}
#Override
public void write(List<? extends Customer> items) throws Exception {
for (Customer customer : items) {
System.out.println(customer);
}
}
};
}
#Bean
public ItemStreamWriter<NewCustomer> customer2() {
System.out.println("Customer #2");
return new ItemStreamWriter<NewCustomer>() {
#Override
public void open(ExecutionContext executionContext) throws ItemStreamException {
}
#Override
public void update(ExecutionContext executionContext) throws ItemStreamException {
}
#Override
public void close() throws ItemStreamException {
}
#Override
public void write(List<? extends NewCustomer> items) throws Exception {
for (NewCustomer customer : items) {
System.out.println(customer);
}
}
};
}
#Bean
public Step classifierCompositeWriterStep() throws IOException {
return this.stepBuilderFactory.get("compositeWriterStep")
.<Customer, Customer>chunk(10)
.reader(this.classifierCompositeWriterItemReader())
.writer(this.compositeItemWriter())
.stream(this.customer1())
.stream(this.customer2())
.build();
}
#Bean
public Job classifierCompositeWriterJob() throws IOException {
return this.jobBuilderFactory.get("compositeWriterJob")
.start(this.classifierCompositeWriterStep())
.build();
}
public static void main(String[] args) {
SpringApplication.run(ClassifierCompositeItemApplication.class, args);
}
}
CustomerClassifier.java
#Data
public class CustomerClassifier implements Classifier<Customer, ItemStreamWriter<? super Customer>> {
private static final long serialVersionUID = 1L;
private final ItemStreamWriter<Customer> fileItemWriter;
private final ItemStreamWriter<Customer> jdbcItemWriter;
#Override
public ItemStreamWriter<? super Customer> classify(Customer customer) {
if (customer.getState().matches("^[A-M].*")) {
return fileItemWriter;
} else {
return jdbcItemWriter;
}
}
}
Customer.java
#Data
public class Customer implements Serializable {
private String firstName;
private String middleInitial;
private String lastName;
private String address;
private String city;
private String state;
private String zip;
#Override
public String toString() {
return "Customer{" + ", firstName='" + firstName + '\'' + ", middleInitial='" + middleInitial + '\''
+ ", lastName='" + lastName + '\'' + ", address='" + address + '\'' + ", city='" + city + '\''
+ ", state='" + state + '\'' + ", zip='" + zip + '\'' + '}';
}
}
Schema.sql
CREATE TABLE springbatch.TBL_CUSTOMER_WRITER (
firstname varchar NULL,
middleinitial varchar NULL,
lastname varchar NULL,
address varchar NULL,
city varchar NULL,
state varchar NULL,
zipcode varchar NULL
)
WITH (
OIDS=FALSE
) ;
Customer.csv
Richard,N,Darrow,5570 Isabella Ave,St. Louis,IL,58540
Barack,G,Donnelly,7844 S. Greenwood Ave,Houston,CA,38635
Ann,Z,Benes,2447 S. Greenwood Ave,Las Vegas,NY,55366
Laura,9S,Minella,8177 4th Street,Dallas,FL,04119
Erica,Z,Gates,3141 Farnam Street,Omaha,CA,57640
Warren,L,Darrow,4686 Mt. Lee Drive,St. Louis,NY,94935
Warren,M,Williams,6670 S. Greenwood Ave,Hollywood,FL,37288
Harry,T,Smith,3273 Isabella Ave,Houston,FL,97261
Steve,O,James,8407 Infinite Loop Drive,Las Vegas,WA,90520
Erica,Z,Neuberger,513 S. Greenwood Ave,Miami,IL,12778
Aimee,C,Hoover,7341 Vel Avenue,Mobile,AL,35928
Jonas,U,Gilbert,8852 In St.,Saint Paul,MN,57321
Regan,M,Darrow,4851 Nec Av.,Gulfport,MS,33193
Stuart,K,Mckenzie,5529 Orci Av.,Nampa,ID,18562
Sydnee,N,Robinson,894 Ornare. Ave,Olathe,KS,25606
You are registering your delegate item writers as streams here:
.stream(this.customer1())
.stream(this.customer2())
But those are not ItemStreams. You need to change the return type of the methods customer1() and customer2() to ItemStreamWriter.
EDIT: Add an example:
import java.util.Arrays;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.ClassifierCompositeItemWriter;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.item.support.builder.ClassifierCompositeItemWriterBuilder;
import org.springframework.classify.Classifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
#EnableBatchProcessing
public class MyJob {
#Bean
public ItemReader<Customer> itemReader() {
return new ListItemReader<>(Arrays.asList(new Customer("foo"), new Customer("bar")));
}
#Bean
public ItemWriter<Customer> fooWriter() {
return items -> {
for (Customer item : items) {
System.out.println("foo writer: item " + item.name);
}
};
}
#Bean
public ItemWriter<Customer> barWriter() {
return items -> {
for (Customer item : items) {
System.out.println("bar writer: item " + item.name);
}
};
}
#Bean
public ClassifierCompositeItemWriter<Customer> classifierCompositeItemWriter() {
final Classifier<Customer, ItemWriter<? super Customer>> classifier =
new CustomerClassifier(this.fooWriter(), this.barWriter());
return new ClassifierCompositeItemWriterBuilder<Customer>()
.classifier(classifier)
.build();
}
#Bean
public Job job(JobBuilderFactory jobs, StepBuilderFactory steps) {
return jobs.get("job")
.start(steps.get("step")
.<Customer, Customer>chunk(5)
.reader(itemReader())
.writer(classifierCompositeItemWriter())
.build())
.build();
}
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(MyJob.class);
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
jobLauncher.run(job, new JobParameters());
}
static class Customer {
String name;
public Customer(String name) {
this.name = name;
}
}
static class CustomerClassifier implements Classifier<Customer, ItemWriter<? super Customer>> {
private ItemWriter<? super Customer> fooItemWriter;
private ItemWriter<? super Customer> barItemWriter;
public CustomerClassifier(ItemWriter<? super Customer> fooItemWriter, ItemWriter<? super Customer> barItemWriter) {
this.fooItemWriter = fooItemWriter;
this.barItemWriter = barItemWriter;
}
#Override
public ItemWriter<? super Customer> classify(Customer customer) {
return customer.name.startsWith("f") ? fooItemWriter : barItemWriter;
}
}
}
This prints:
foo writer: item foo
bar writer: item bar

JpaPagingItemReader - endless Loop on failed read

Hi I am getting endless loop when exception was thrown.
Is there something wrong to the transactionmanager I am using?
Thanks!
Below is my job setup
#Bean
public ItemReader<Entity> bizAppReader() {
#SuppressWarnings("serial")
Map<String, Object> map = new HashMap<String, Object>() {{
put("status", Arrays.asList(new ApplicationStatus[] {ApplicationStatus.COMPLETED, ApplicationStatus.PENDING, ApplicationStatus.INITIATE}));
}};
return new JpaPagingItemReaderBuilder<Entity>()
.name("bizAppJpaReader")
.entityManagerFactory(entityManagerFactory)
.queryString("from Entity b where b.status in (:status)").parameterValues(map)
.build();
}
#Bean
public Step bizApplicatonStep(#Value("${batch.chunk_size:10}") int chunkSize,
ItemReader<Entity> bizAppReader,
ItemProcessor<Entity, Map<ApplicationStatus, String>> bizAppProcessor,
ItemWriter<Map<ApplicationStatus, String>> bizAppWriter) {
return stepBuilderFactory.get("bizApplicatonStep")
.<Entity, Map<ApplicationStatus, String>>chunk(chunkSize).reader(bizAppReader)
.processor(bizAppProcessor).writer(bizAppWriter)
.faultTolerant()
.skip(Exception.class)
.noRollback(Exception.class)
.skipPolicy((e, i) -> {
log.error("Exception occurred : ", e);
return true;
})
.allowStartIfComplete(true)
.build();
}
#Slf4j
#SpringBootApplication
#EnableBatchProcessing
public class BatchApplication extends DefaultBatchConfigurer {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(BatchApplication.class, args);
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job;
JobParameters jobParameters;
if (ArrayUtils.isNotEmpty(args) && args[0].equals("bookingJob")) {
job = context.getBean("bookingJob", Job.class);
jobParameters = new JobParametersBuilder()
.addString("startMonth", args[1])
.addString("endMonth", args[2])
.toJobParameters();
} else {
job = context.getBean("bizApplicationJob", Job.class);
jobParameters = new JobParametersBuilder().toJobParameters();
}
JobExecution jobExecution = jobLauncher.run(job, jobParameters);
if (BatchStatus.FAILED.equals(jobExecution.getStatus())) {
log.error("job execution completed exit code is : 1 ");
System.exit(1);
}
log.info("job execution completed exit code is : 0 ");
}
#Override
protected JobRepository createJobRepository() throws Exception {
return new MapJobRepositoryFactoryBean(new ResourcelessTransactionManager()).getObject();
}
}
Stacktrace:
10:13:20.657 [main] ERROR c.u.pweb.bizacct.batch.BizJobConfig - Exception occurred :
java.lang.IllegalStateException: Transaction already active
at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:52)
-===================== Updated application to replicate =====================
this happen if the reader failed to read with invalid Enum value from DB.
#Slf4j
#SpringBootApplication
#EnableBatchProcessing
public class DemoBatchApplication extends DefaultBatchConfigurer {
public static void main(String[] args) {
SpringApplication.run(DemoBatchApplication.class, args);
}
#Override
public void setDataSource(DataSource dataSource) {
super.setDataSource(null);
}
private final EntityManagerFactory entityManagerFactory;
public DemoBatchApplication(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
#Override
public PlatformTransactionManager getTransactionManager() {
return new JpaTransactionManager(this.entityManagerFactory);
}
}
enum
public enum ApplicationStatus {
MA, AS, SH, CP, AA, AP
}
table
#Entity
#Table(name = "APP_TBL_TEST")
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class ApplicationEntity {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(name = "ID", length = 50)
private String id;
#Column(name = "APPLICATION_NAME", unique = true, length = 50)
private String referenceNumber;
#Column(name = "STATUS")
#Enumerated(EnumType.STRING)
private ApplicationStatus status;
}
batch config
#Configuration
#Slf4j
#Data
public class ApplicationBatchCfg {
#Autowired
public EntityManagerFactory entityManagerFactory;
#Autowired
public JobBuilderFactory jobBuilderFactory;
#Autowired
public StepBuilderFactory stepBuilderFactory;
#Bean
public Step applicationStep(#Value("${batch.chunk_size:5}") int chunkSize,
ItemReader<ApplicationEntity> appReader,
ItemProcessor<ApplicationEntity, String> appProcessor,
ItemWriter<String> appWriter){
return stepBuilderFactory.get("applicatonStep")
.<ApplicationEntity, String>chunk(chunkSize)
.reader(appReader)
.processor(appProcessor)
.writer(appWriter)
.faultTolerant()
.skip(Exception.class)
.noRollback(Exception.class)
.skipPolicy((e, i) -> {
log.error("Exception occurred : ", e);
return true;
})
.skipLimit(5)
.allowStartIfComplete(true)
.build();
}
#Bean
public Job applicationJob(Step applicationStep) {
return jobBuilderFactory.get("applicationJob").incrementer(new RunIdIncrementer())
.start(applicationStep).build();
}
#Bean
public ItemReader<ApplicationEntity> appReader() {
return new JpaPagingItemReaderBuilder<ApplicationEntity>()
.name("appReader")
.entityManagerFactory(entityManagerFactory)
.queryString("from ApplicationEntity")
.pageSize(3)
.build();
}
#Bean
public ItemProcessor<ApplicationEntity, String> appProcessor(){
return new ItemProcessor<ApplicationEntity, String>() {
#Override
public String process(ApplicationEntity item) throws Exception {
//nothing for demo only
log.info("item {}", item);
return null;
}
};
}
#Bean
public ItemWriter<String> appWriter() {
return new ItemWriter<String>() {
#Override
public void write(List<? extends String> items) throws Exception {
//nothing for demo only
}
};
}
}
inside yml
spring:
jpa:
properties:
hibernate:
show_sql: true
use_sql_comments : true
format_sql: true
dialect: org.hibernate.dialect.Oracle10gDialect
datasource:
useWallet: false
url: jdbc:oracle:thin:#DB12397:1521:azd
platform: oracle
username: user
password: pwd

Why the intemReader is always sending the exact same value to CustomItemProcessor

Why does the itemReader method is always sending the exact same file name to be processed in CustomItemProcessor?
As far as I understand, since I settup reader as #Scope and I set more than 1 in chunk, I was expecting the "return s" to move forward to next value from String array.
Let me clarify my question with a debug example in reader method:
1 - the variable stringArray is filled in with 3 file names (f1.txt, f2.txt and f3.txt)
2 - "return s" is evoked with s = f1.txt
3 - "return s" evoked again before evoked customItemProcessor method (perfect untill here since chunk = 2)
4 - looking at s it contains f1.txt again (different from what I expected. I expected f2.txt)
5 and 6 - runs processor with same name f1.tx (it should work correctly if the second turn of "return s" would contain f2.txt)
7 - writer method works as expected (processedFiles contain twice the two names processed in customItemProcessor f1.txt and f1.txt again since same name was processed twice)
CustomItemReader
public class CustomItemReader implements ItemReader<String> {
#Override
public String read() throws Exception, UnexpectedInputException,
ParseException, NonTransientResourceException {
String[] stringArray;
try (Stream<Path> stream = Files.list(Paths.get(env
.getProperty("my.path")))) {
stringArray = stream.map(String::valueOf)
.filter(path -> path.endsWith("out"))
.toArray(size -> new String[size]);
}
//*** the problem is here
//every turn s variable receives the first file name from the stringArray
if (stringArray.length > 0) {
for (String s : stringArray) {
return s;
}
} else {
log.info("read method - no file found");
return null;
}
return null;
}
CustomItemProcessor
public class CustomItemProcessor implements ItemProcessor<String , String> {
#Override
public String process(String singleFileToProcess) throws Exception {
log.info("process method: " + singleFileToProcess);
return singleFileToProcess;
}
}
CustomItemWriter
public class CustomItemWriter implements ItemWriter<String> {
private static final Logger log = LoggerFactory
.getLogger(CustomItemWriter.class);
#Override
public void write(List<? extends String> processedFiles) throws Exception {
processedFiles.stream().forEach(
processedFile -> log.info("**** write method"
+ processedFile.toString()));
FileSystem fs = FileSystems.getDefault();
for (String s : processedFiles) {
Files.deleteIfExists(fs.getPath(s));
}
}
Configuration
#Configuration
#ComponentScan(...
#EnableBatchProcessing
#EnableScheduling
#PropertySource(...
public class BatchConfig {
#Autowired
private JobBuilderFactory jobBuilderFactory;
#Autowired
private StepBuilderFactory stepBuilderFactory;
#Autowired
private JobRepository jobRepository;
#Bean
public TaskExecutor getTaskExecutor() {
return new TaskExecutor() {
#Override
public void execute(Runnable task) {
}
};
}
//I can see the number in chunk reflects how many time customReader is triggered before triggers customProcesser
#Bean
public Step step1(ItemReader<String> reader,
ItemProcessor<String, String> processor, ItemWriter<String> writer) {
return stepBuilderFactory.get("step1").<String, String> chunk(2)
.reader(reader).processor(processor).writer(writer)
.allowStartIfComplete(true).build();
}
#Bean
#Scope
public ItemReader<String> reader() {
return new CustomItemReader();
}
#Bean
public ItemProcessor<String, String> processor() {
return new CustomItemProcessor();
}
#Bean
public ItemWriter<String> writer() {
return new CustomItemWriter();
}
#Bean
public Job job(Step step1) throws Exception {
return jobBuilderFactory.get("job1").incrementer(new RunIdIncrementer()).start(step1).build();
}
Scheduler
#Component
public class QueueScheduler {
private static final Logger log = LoggerFactory
.getLogger(QueueScheduler.class);
private Job job;
private JobLauncher jobLauncher;
#Autowired
public QueueScheduler(JobLauncher jobLauncher, #Qualifier("job") Job job){
this.job = job;
this.jobLauncher = jobLauncher;
}
#Scheduled(fixedRate=60000)
public void runJob(){
try{
jobLauncher.run(job, new JobParameters());
}catch(Exception ex){
log.info(ex.getMessage());
}
}
}
Your issue is that you are relying on an internal loop to iterate over the items instead of letting Spring Batch do it for you by calling ItemReader#read multiple times.
What I'd recommend is changing your reader to the something like the following:
public class JimsItemReader implements ItemStreamReader {
private String[] items;
private int curIndex = -1;
#Override
public void open(ExecutionContext ec) {
curIndex = ec.getInt("curIndex", -1);
String[] stringArray;
try (Stream<Path> stream = Files.list(Paths.get(env.getProperty("my.path")))) {
stringArray = stream.map(String::valueOf)
.filter(path -> path.endsWith("out"))
.toArray(size -> new String[size]);
}
}
#Override
public void update(ExecutionContext ec) {
ec.putInt("curIndex", curIndex);
}
#Override
public String read() {
if (curIndex < items.length) {
curIndex++;
return items[curIndex];
} else {
return null;
}
}
}
The above example should loop through the items of your array as they are read. It also should be restartable in that we're storing the index in the ExecutionContext so if the job is restarted after a failure, you'll restart where you left off.

Resources