diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java index d7339459c..951410235 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2022 the original author or authors. + * Copyright 2006-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,15 @@ import org.springframework.batch.core.scope.context.ChunkContext; /** * Listener interface for the lifecycle of a chunk. A chunk can be thought of as a * collection of items that are committed together. + *

+ * {@link ChunkListener} shouldn't throw exceptions and expect continued processing, they + * must be handled in the implementation or the step will terminate. * * @author Lucas Ward * @author Michael Minella * @author Mahmoud Ben Hassine * @author Parikshit Dutta + * @author Injae Kim */ public interface ChunkListener extends StepListener { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java index 78c5fadc9..c5a5e5eb5 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2023 the original author or authors. + * Copyright 2006-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -689,10 +689,6 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { /** * ChunkListener that wraps exceptions thrown from the ChunkListener in * {@link FatalStepExecutionException} to force termination of StepExecution - *

- * ChunkListeners shoulnd't throw exceptions and expect continued processing, they - * must be handled in the implementation or the step will terminate - * */ private static class TerminateOnExceptionChunkListenerDelegate implements ChunkListener { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java index 09743e3c6..d4f6137db 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2023 the original author or authors. + * Copyright 2006-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -328,6 +328,87 @@ class SimpleStepFactoryBeanTests { assertTrue(writeListener.trail.startsWith("1234"), "Listener order not as expected: " + writeListener.trail); } + @Test + void testChunkListenersThrowException() throws Exception { + String[] items = new String[] { "1", "2", "3", "4", "5", "6", "7" }; + int commitInterval = 3; + + SimpleStepFactoryBean factory = getStepFactory(items); + class AssertingWriteListener extends StepListenerSupport { + + String trail = ""; + + @Override + public void beforeWrite(Chunk chunk) { + trail = trail + "2"; + } + + @Override + public void afterWrite(Chunk items) { + trail = trail + "3"; + } + + } + class CountingChunkListener implements ChunkListener { + + int beforeCount = 0; + + int afterCount = 0; + + int failedCount = 0; + + private final AssertingWriteListener writeListener; + + public CountingChunkListener(AssertingWriteListener writeListener) { + super(); + this.writeListener = writeListener; + } + + @Override + public void afterChunk(ChunkContext context) { + writeListener.trail = writeListener.trail + "4"; + afterCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + @Override + public void beforeChunk(ChunkContext context) { + writeListener.trail = writeListener.trail + "1"; + beforeCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + @Override + public void afterChunkError(ChunkContext context) { + writeListener.trail = writeListener.trail + "5"; + failedCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + } + AssertingWriteListener writeListener = new AssertingWriteListener(); + CountingChunkListener chunkListener = new CountingChunkListener(writeListener); + factory.setListeners(new StepListener[] { chunkListener, writeListener }); + factory.setCommitInterval(commitInterval); + + AbstractStep step = (AbstractStep) factory.getObject(); + + job.setSteps(Collections.singletonList((Step) step)); + + JobExecution jobExecution = repository.createJobExecution(job.getName(), new JobParameters()); + job.execute(jobExecution); + + assertEquals(BatchStatus.FAILED, jobExecution.getStatus()); + assertEquals("1", reader.read()); + assertEquals(0, written.size()); + + assertEquals(0, chunkListener.afterCount); + assertEquals(1, chunkListener.beforeCount); + assertEquals(1, chunkListener.failedCount); + assertEquals("15", writeListener.trail); + assertTrue(writeListener.trail.startsWith("15"), "Listener order not as expected: " + writeListener.trail); + } + /* * Commit interval specified is not allowed to be zero or negative. */ diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc index 142cc7772..023dcaee4 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc @@ -130,6 +130,9 @@ You can apply a `ChunkListener` when there is no chunk declaration. The `Tasklet responsible for calling the `ChunkListener`, so it applies to a non-item-oriented tasklet as well (it is called before and after the tasklet). +A `ChunkListener` is not designed to throw checked exceptions. Errors must be handled in the +implementation or the step will terminate. + [[itemReadListener]] == `ItemReadListener`