OPEN - issue BATCH-404: FactoryBeans for step configuration

http://jira.springframework.org/browse/BATCH-404

Let the factory bean take care of chunk and item listener callbacks.  ItemOrientedStep is much simpler as a result.
This commit is contained in:
dsyer
2008-03-02 15:03:12 +00:00
parent 81b9b5ebc8
commit bbeeee32d3
8 changed files with 204 additions and 114 deletions

View File

@@ -19,17 +19,17 @@ import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.domain.BatchListener;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.ItemSkipPolicy;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.JobInterruptedException;
import org.springframework.batch.core.domain.StepContribution;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepListener;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.runtime.ExitStatusExceptionClassifier;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.step.support.ListenerMulticaster;
import org.springframework.batch.execution.listener.CompositeStepListener;
import org.springframework.batch.execution.step.support.NeverSkipItemSkipPolicy;
import org.springframework.batch.execution.step.support.SimpleExitStatusExceptionClassifier;
import org.springframework.batch.execution.step.support.StepInterruptionPolicy;
@@ -95,7 +95,7 @@ public class ItemOrientedStep extends AbstractStep {
private CompositeItemStream stream = new CompositeItemStream();
private ListenerMulticaster listener = new ListenerMulticaster();
private CompositeStepListener listener = new CompositeStepListener();
private JobRepository jobRepository;
@@ -166,27 +166,43 @@ public class ItemOrientedStep extends AbstractStep {
*/
public void setStreams(ItemStream[] streams) {
for (int i = 0; i < streams.length; i++) {
stream.register(streams[i]);
registerStream(streams[i]);
}
}
/**
* Register each of the objects as listeners. The {@link ItemOrientedStep}
* accepts listeners of type {@link BatchListener}. The {@link ItemReader}
* and {@link ItemWriter} are automatically registered, but it doesn't hurt
* to also register them here. Injected dependencies of the reader and
* writer are not automatically registered, so if you implement
* {@link ItemWriter} using delegation to another object which itself is a
* {@link BatchListener}, you need to register the delegate here.
* Register a single {@link ItemStream} for callbacks to the stream
* interface.
* @param stream
*/
public void registerStream(ItemStream stream) {
this.stream.register(stream);
}
/**
* Register each of the objects as listeners. If the {@link Tasklet} itself
* implements this interface it will be registered automatically, but its
* injected dependencies will not be. This is a good way to get access to
* job parameters and execution context if the tasklet is parameterised.
*
* @param listeners an array of listener objects of known types.
*/
public void setListeners(BatchListener[] listeners) {
public void setStepListeners(StepListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
listener.register(listeners[i]);
registerStepListener(listeners[i]);
}
}
/**
* Register a step listener for callbacks at the appropriate stages in a
* step execution.
*
* @param listener a {@link StepListener}
*/
public void registerStepListener(StepListener listener) {
this.listener.register(listener);
}
/**
* The {@link RepeatOperations} to use for the outer loop of the batch
* processing. Should be set up by the caller through a factory. Defaults to
@@ -314,9 +330,7 @@ public class ItemOrientedStep extends AbstractStep {
try {
itemReader.mark();
listener.beforeChunk();
result = processChunk(contribution);
listener.afterChunk();
contribution.incrementCommitCount();
// If the step operations are asynchronous then we need
@@ -481,14 +495,14 @@ public class ItemOrientedStep extends AbstractStep {
if (itemReader instanceof ItemStream) {
stream.register((ItemStream) itemReader);
}
if (itemReader instanceof BatchListener) {
listener.register((BatchListener) itemReader);
if (itemReader instanceof StepListener) {
listener.register((StepListener) itemReader);
}
if (itemWriter instanceof ItemStream) {
stream.register((ItemStream) itemWriter);
}
if (itemWriter instanceof BatchListener) {
listener.register((BatchListener) itemWriter);
if (itemWriter instanceof StepListener) {
listener.register((StepListener) itemWriter);
}
}
@@ -572,31 +586,11 @@ public class ItemOrientedStep extends AbstractStep {
private ExitStatus execute() throws Exception {
if (retryCallback == null) {
Object item = null;
try {
listener.beforeRead();
item = itemReader.read();
listener.afterRead(item);
}
catch (Exception ex) {
listener.onReadError(ex);
throw ex;
}
Object item = itemReader.read();
if (item == null) {
return ExitStatus.FINISHED;
}
try {
listener.beforeWrite(item);
itemWriter.write(item);
listener.afterWrite();
}
catch (Exception e) {
listener.onWriteError(e, item);
// Re-throw the exception so that the surrounding transaction
// rolls back if there is one
throw e;
}
itemWriter.write(item);
return ExitStatus.CONTINUABLE;
}

View File

@@ -47,6 +47,7 @@ public class TaskletStep extends AbstractStep implements Step, InitializingBean,
private Tasklet tasklet;
private JobRepository jobRepository;
private CompositeStepListener listener = new CompositeStepListener();
/**
@@ -65,29 +66,19 @@ public class TaskletStep extends AbstractStep implements Step, InitializingBean,
}
/**
* Register each of the objects as listeners. The {@link TaskletStep}
* accepts listeners of type {@link StepListener}. If the {@link Tasklet}
* itself implements this interface it will be registered automatically, but
* its injected dependencies will not be. This is a good way to get access
* to job parameters and execution context if the tasklet is parameterised.
* Register each of the objects as listeners. If the {@link Tasklet} itself
* implements this interface it will be registered automatically, but its
* injected dependencies will not be. This is a good way to get access to
* job parameters and execution context if the tasklet is parameterised.
*
* @param listeners an array of listener objects of known types.
*/
public void setListeners(StepListener[] listeners) {
public void setStepListeners(StepListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
this.listener.register(listeners[i]);
}
}
/**
* Register the objects as a listener.
* @see #setListeners(Object[])
* @param listener the listener to set
*/
public void setListener(StepListener listener) {
this.listener.register(listener);
}
/**
* Check mandatory properties.
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()

View File

@@ -17,8 +17,16 @@ package org.springframework.batch.execution.step.support;
import org.springframework.batch.core.domain.BatchListener;
import org.springframework.batch.core.domain.Step;
import org.springframework.batch.core.domain.StepListener;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.reader.DelegatingItemReader;
import org.springframework.batch.item.writer.DelegatingItemWriter;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.exception.handler.SimpleLimitExceptionHandler;
import org.springframework.batch.repeat.listener.RepeatListenerSupport;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.repeat.support.TaskExecutorRepeatTemplate;
import org.springframework.core.task.TaskExecutor;
@@ -35,6 +43,8 @@ public class DefaultStepFactoryBean extends SimpleStepFactoryBean {
private BatchListener[] listeners = new BatchListener[0];
private ListenerMulticaster listener = new ListenerMulticaster();
private TaskExecutor taskExecutor;
/**
@@ -59,7 +69,7 @@ public class DefaultStepFactoryBean extends SimpleStepFactoryBean {
public void setListeners(BatchListener[] listeners) {
this.listeners = listeners;
}
/**
* Public setter for the {@link TaskExecutor}. If this is set, then it will
* be used to execute the chunk processing inside the {@link Step}.
@@ -77,9 +87,74 @@ public class DefaultStepFactoryBean extends SimpleStepFactoryBean {
protected void applyConfiguration(ItemOrientedStep step) {
super.applyConfiguration(step);
step.setListeners(listeners);
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof StepListener) {
step.registerStepListener((StepListener) listener);
}
else {
this.listener.register(listener);
}
}
ItemReader itemReader = getItemReader();
ItemWriter itemWriter = getItemWriter();
// Since we are going to wrap these things with listener callbacks we
// need to register them here because the step will not know we did
// that.
if (itemReader instanceof ItemStream) {
step.registerStream((ItemStream) itemReader);
}
if (itemReader instanceof StepListener) {
step.registerStepListener((StepListener) itemReader);
}
if (itemWriter instanceof ItemStream) {
step.registerStream((ItemStream) itemWriter);
}
if (itemWriter instanceof StepListener) {
step.registerStepListener((StepListener) itemWriter);
}
step.setItemReader(new DelegatingItemReader(itemReader) {
public Object read() throws Exception {
try {
listener.beforeRead();
Object item = super.read();
listener.afterRead(item);
return item;
}
catch (Exception e) {
listener.onReadError(e);
throw e;
}
}
});
step.setItemWriter(new DelegatingItemWriter(itemWriter) {
public void write(Object item) throws Exception {
try {
listener.beforeWrite(item);
super.write(item);
listener.afterWrite();
}
catch (Exception e) {
listener.onWriteError(e, item);
throw e;
}
}
});
RepeatTemplate stepOperations = new RepeatTemplate();
stepOperations.setListener(new RepeatListenerSupport() {
public void open(RepeatContext context) {
listener.beforeChunk();
}
public void close(RepeatContext context) {
listener.afterChunk();
}
});
if (taskExecutor != null) {
TaskExecutorRepeatTemplate repeatTemplate = new TaskExecutorRepeatTemplate();

View File

@@ -22,7 +22,6 @@ import java.util.List;
import junit.framework.TestCase;
import org.springframework.batch.core.domain.BatchListener;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobInstance;
@@ -358,7 +357,7 @@ public class ItemOrientedStepTests extends TestCase {
}
public void testDirectlyInjectedListener() throws Exception {
itemOrientedStep.setListeners(new BatchListener[] {new StepListenerSupport() {
itemOrientedStep.registerStepListener(new StepListenerSupport() {
public void beforeStep(StepExecution stepExecution) {
list.add("foo");
}
@@ -366,7 +365,7 @@ public class ItemOrientedStepTests extends TestCase {
list.add("bar");
return null;
}
}});
});
JobExecution jobExecution = new JobExecution(jobInstance);
StepExecution stepExecution = new StepExecution(itemOrientedStep, jobExecution);
itemOrientedStep.execute(stepExecution);
@@ -374,26 +373,28 @@ public class ItemOrientedStepTests extends TestCase {
}
public void testListenerCalledBeforeStreamOpened() throws Exception {
itemOrientedStep.setListeners(new BatchListener[] {new MockRestartableItemReader() {
MockRestartableItemReader reader = new MockRestartableItemReader() {
public void beforeStep(StepExecution stepExecution) {
list.add("foo");
}
public void open(ExecutionContext executionContext) throws StreamException {
assertEquals(1, list.size());
}
}});
};
itemOrientedStep.setStreams(new ItemStream[] {reader});
itemOrientedStep.registerStepListener(reader);
StepExecution stepExecution = new StepExecution(itemOrientedStep, new JobExecution(jobInstance));
itemOrientedStep.execute(stepExecution);
assertEquals(1, list.size());
}
public void testDirectlyInjectedListenerOnError() throws Exception {
itemOrientedStep.setListeners(new StepListener[] {new StepListenerSupport() {
itemOrientedStep.registerStepListener(new StepListenerSupport() {
public ExitStatus onErrorInStep(Throwable e) {
list.add(e);
return null;
}
}});
});
itemOrientedStep.setItemReader(new MockRestartableItemReader() {
public Object read() throws Exception {
throw new RuntimeException("FOO");

View File

@@ -11,6 +11,7 @@ import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.JobInterruptedException;
import org.springframework.batch.core.domain.JobParameters;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepListener;
import org.springframework.batch.core.listener.StepListenerSupport;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.job.JobSupport;
@@ -25,8 +26,8 @@ public class TaskletStepTests extends TestCase {
private List list = new ArrayList();
protected void setUp() throws Exception {
stepExecution = new StepExecution(new StepSupport("stepName"), new JobExecution(new JobInstance(
new Long(0L), new JobParameters(), new JobSupport("testJob")), new Long(12)));
stepExecution = new StepExecution(new StepSupport("stepName"), new JobExecution(new JobInstance(new Long(0L),
new JobParameters(), new JobSupport("testJob")), new Long(12)));
}
public void testTaskletMandatory() throws Exception {
@@ -89,7 +90,8 @@ public class TaskletStepTests extends TestCase {
try {
step.execute(stepExecution);
fail("Expected BatchCriticalException");
} catch (InfrastructureException e){
}
catch (InfrastructureException e) {
assertEquals("foo", e.getCause().getMessage());
}
assertEquals(BatchStatus.UNKNOWN, stepExecution.getStatus());
@@ -105,15 +107,16 @@ public class TaskletStepTests extends TestCase {
public void testSuccessfulExecutionWithListener() throws Exception {
TaskletStep step = new TaskletStep(new StubTasklet(false, false), new JobRepositorySupport());
step.setListener(new StepListenerSupport() {
step.setStepListeners(new StepListener[] { new StepListenerSupport() {
public void beforeStep(StepExecution context) {
list.add("open");
}
public ExitStatus afterStep() {
list.add("close");
return ExitStatus.CONTINUABLE;
}
});
} });
step.execute(stepExecution);
assertEquals(2, list.size());
}
@@ -138,13 +141,13 @@ public class TaskletStepTests extends TestCase {
private final boolean throwException;
private final boolean assertStepContext;
private StepExecution stepExecution;
public StubTasklet(boolean exitFailure, boolean throwException) {
this(exitFailure, throwException, false);
}
public StubTasklet(boolean exitFailure, boolean throwException, boolean assertStepContext) {
this.exitFailure = exitFailure;
this.throwException = throwException;
@@ -159,14 +162,14 @@ public class TaskletStepTests extends TestCase {
if (exitFailure) {
return ExitStatus.FAILED;
}
if (assertStepContext) {
assertNotNull(this.stepExecution);
}
return ExitStatus.FINISHED;
}
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.batch.execution.launch;
package org.springframework.batch.execution.step.support;
import java.util.ArrayList;
import java.util.Arrays;
@@ -36,8 +36,7 @@ import org.springframework.batch.execution.repository.dao.MapJobInstanceDao;
import org.springframework.batch.execution.repository.dao.MapStepExecutionDao;
import org.springframework.batch.execution.step.AbstractStep;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.execution.step.support.AbstractStepFactoryBean;
import org.springframework.batch.execution.step.support.SimpleStepFactoryBean;
import org.springframework.batch.execution.step.support.DefaultStepFactoryBean;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.reader.ListItemReader;
@@ -48,11 +47,12 @@ import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
public class SimpleJobTests extends TestCase {
public class DefaultStepFactoryBeanTests extends TestCase {
private List recovered = new ArrayList();
private SimpleJobRepository repository = new SimpleJobRepository(new MapJobInstanceDao(), new MapJobExecutionDao(), new MapStepExecutionDao());
private SimpleJobRepository repository = new SimpleJobRepository(new MapJobInstanceDao(), new MapJobExecutionDao(),
new MapStepExecutionDao());
private List processed = new ArrayList();
@@ -74,45 +74,44 @@ public class SimpleJobTests extends TestCase {
MapStepExecutionDao.clear();
}
private AbstractStep getStep(String arg) throws Exception {
private DefaultStepFactoryBean getStep(String arg) throws Exception {
return getStep(new String[] { arg });
}
private AbstractStep getStep(String arg0, String arg1) throws Exception {
private DefaultStepFactoryBean getStep(String arg0, String arg1) throws Exception {
return getStep(new String[] { arg0, arg1 });
}
private ItemOrientedStep getStep(String[] args) throws Exception {
AbstractStepFactoryBean factory = new SimpleStepFactoryBean();
private DefaultStepFactoryBean getStep(String[] args) throws Exception {
DefaultStepFactoryBean factory = new DefaultStepFactoryBean();
factory.setSingleton(false);
List items = TransactionAwareProxyFactory.createTransactionalList();
items.addAll(Arrays.asList(args));
provider = new ListItemReader(items);
factory.setItemReader(provider);
factory.setItemWriter(processor);
factory.setJobRepository(repository);
factory.setTransactionManager(new ResourcelessTransactionManager());
factory.setBeanName("stepName");
ItemOrientedStep step = (ItemOrientedStep) factory.getObject();
// step.setItemRecoverer(new ItemRecoverer() {
// public boolean recover(Object item, Throwable cause) {
// recovered.add(item);
// assertTrue(TransactionSynchronizationManager.isActualTransactionActive());
// return true;
// }
// });
return step;
// step.setItemRecoverer(new ItemRecoverer() {
// public boolean recover(Object item, Throwable cause) {
// recovered.add(item);
// assertTrue(TransactionSynchronizationManager.isActualTransactionActive());
// return true;
// }
// });
return factory;
}
public void testSimpleJob() throws Exception {
job.setSteps(new ArrayList());
AbstractStep step = getStep("foo", "bar");
AbstractStep step = (AbstractStep) getStep("foo", "bar").getObject();
step.setName("step1");
job.addStep(step);
step = getStep("spam");
step = (AbstractStep) getStep("spam").getObject();
step.setName("step2");
job.addStep(step);
@@ -126,7 +125,7 @@ public class SimpleJobTests extends TestCase {
assertTrue(processed.contains("foo"));
}
public void testSimpleJobWithRecovery() throws Exception {
public void testSimpleJobWithItemListeners() throws Exception {
final List throwables = new ArrayList();
@@ -144,30 +143,25 @@ public class SimpleJobTests extends TestCase {
* is recovered ("skipped") on the second attempt (see retry policy
* definition above)...
*/
ItemOrientedStep step = getStep(new String[] { "foo", "bar", "spam" });
// Tasklet module = getTasklet(new String[] { "foo", "bar", "spam" });
// RepeatOperationsStep step = new RepeatOperationsStep();
step.setChunkOperations(chunkOperations);
step.setItemWriter(new AbstractItemWriter() {
DefaultStepFactoryBean factory = getStep(new String[] { "foo", "bar", "spam" });
factory.setItemWriter(new AbstractItemWriter() {
public void write(Object data) throws Exception {
throw new RuntimeException("Error!");
}
});
step.setListeners(new BatchListener[]{new ItemListenerSupport(){
factory.setListeners(new BatchListener[] { new ItemListenerSupport() {
public void onReadError(Exception ex) {
recovered.add(ex);
}
public void onWriteError(Exception ex, Object item) {
recovered.add(ex);
}
}});
} });
ItemOrientedStep step = (ItemOrientedStep) factory.getObject();
step.setChunkOperations(chunkOperations);
job.setSteps(Collections.singletonList(step));
JobExecution jobExecution = repository.createJobExecution(job, new JobParameters());
@@ -179,9 +173,11 @@ public class SimpleJobTests extends TestCase {
assertEquals(null, provider.read());
assertEquals(3, recovered.size());
}
// TODO: test recovery and stateful retry
public void testExceptionTerminates() throws Exception {
ItemOrientedStep step = getStep(new String[] { "foo", "bar", "spam" });
ItemOrientedStep step = (ItemOrientedStep) getStep(new String[] { "foo", "bar", "spam" }).getObject();
step.setName("exceptionStep");
step.setItemWriter(new AbstractItemWriter() {
public void write(Object data) throws Exception {
@@ -201,5 +197,5 @@ public class SimpleJobTests extends TestCase {
}
assertEquals(BatchStatus.FAILED, jobExecution.getStatus());
}
}

View File

@@ -31,6 +31,21 @@ import org.springframework.util.Assert;
public class DelegatingItemReader extends AbstractItemReader implements Skippable, InitializingBean {
private ItemReader itemReader;
/**
* Default constructor.
*/
public DelegatingItemReader() {
super();
}
/**
* Convenience constructor for setting mandatory property.
*/
public DelegatingItemReader(ItemReader itemReader) {
this();
this.itemReader = itemReader;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(itemReader, "ItemReader must not be null.");

View File

@@ -16,6 +16,21 @@ public class DelegatingItemWriter implements ItemWriter, InitializingBean {
private ItemWriter writer;
/**
* Default constructor.
*/
public DelegatingItemWriter() {
super();
}
/**
* @param itemWriter
*/
public DelegatingItemWriter(ItemWriter itemWriter) {
this();
this.writer = itemWriter;
}
/**
* Calls {@link #doProcess(Object)} and then writes the result to the
* delegate {@link ItemWriter}.
@@ -23,7 +38,7 @@ public class DelegatingItemWriter implements ItemWriter, InitializingBean {
*
* @see ItemWriter#process(java.lang.Object)
*/
final public void write(Object item) throws Exception {
public void write(Object item) throws Exception {
Object result = doProcess(item);
writer.write(result);
}