Skip to content

Commit 873b742

Browse files
committed
Fix listener API allowing invalid listeners
Before listeners could be created without annotations which then would be ignored during runtime. Now Builders with listeners without annotations will throw a BuilderException containing the IllegalArgumentExceptions of the listeners. Resolves #4808 Signed-off-by: Sacha Leemann <sacha.leemann@yahoo.de>
1 parent 639d64f commit 873b742

File tree

13 files changed

+118
-3
lines changed

13 files changed

+118
-3
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/FlowJobBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ public Job build() {
9393
job.setName(getName());
9494
job.setFlow(flow);
9595
super.enhance(job);
96+
if (!listenerErrors.isEmpty()) {
97+
throw new JobBuilderException(
98+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
99+
}
96100
try {
97101
job.afterPropertiesSet();
98102
}

spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/JobBuilderHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public abstract class JobBuilderHelper<B extends JobBuilderHelper<B>> {
5454

5555
private final CommonJobProperties properties;
5656

57+
protected List<Throwable> listenerErrors = new ArrayList<>();
58+
5759
/**
5860
* Create a new {@link JobBuilderHelper}.
5961
* @param jobRepository the job repository
@@ -83,6 +85,7 @@ public JobBuilderHelper(String name, JobRepository jobRepository) {
8385
*/
8486
protected JobBuilderHelper(JobBuilderHelper<?> parent) {
8587
this.properties = new CommonJobProperties(parent.properties);
88+
this.listenerErrors = parent.listenerErrors;
8689
}
8790

8891
/**
@@ -161,6 +164,10 @@ public B listener(Object listener) {
161164
factory.setDelegate(listener);
162165
properties.addJobExecutionListener((JobExecutionListener) factory.getObject());
163166
}
167+
else {
168+
listenerErrors
169+
.add(new IllegalArgumentException("Missing @BeforeJob or @AfterJob annotations on Listener."));
170+
}
164171

165172
@SuppressWarnings("unchecked")
166173
B result = (B) this;

spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/SimpleJobBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public Job build() {
5353
SimpleJob job = new SimpleJob(getName());
5454
super.enhance(job);
5555
job.setSteps(steps);
56+
if (!listenerErrors.isEmpty()) {
57+
throw new JobBuilderException(
58+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
59+
}
5660
try {
5761
job.afterPropertiesSet();
5862
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilder.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ public TaskletStep build() {
107107

108108
step.setChunkListeners(chunkListeners.toArray(new ChunkListener[0]));
109109

110+
if (!listenerErrors.isEmpty()) {
111+
throw new StepBuilderException(
112+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
113+
}
114+
110115
if (this.transactionManager != null) {
111116
step.setTransactionManager(this.transactionManager);
112117
}
@@ -170,17 +175,21 @@ public B listener(ChunkListener listener) {
170175
@Override
171176
public B listener(Object listener) {
172177
super.listener(listener);
173-
174178
Set<Method> chunkListenerMethods = new HashSet<>();
175179
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeChunk.class));
176180
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunk.class));
177181
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunkError.class));
178182

179183
if (!chunkListenerMethods.isEmpty()) {
184+
listenerErrors.clear();
180185
StepListenerFactoryBean factory = new StepListenerFactoryBean();
181186
factory.setDelegate(listener);
182187
this.listener((ChunkListener) factory.getObject());
183188
}
189+
else if (!listenerErrors.isEmpty()) {
190+
listenerErrors.add(new IllegalArgumentException(
191+
"Missing @BeforeChunk, @AfterChunk or @AfterChunkError annotations on Listener."));
192+
}
184193

185194
return self();
186195
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,21 @@ protected Tasklet createTasklet() {
194194
@Override
195195
public FaultTolerantStepBuilder<I, O> listener(Object listener) {
196196
super.listener(listener);
197-
198197
Set<Method> skipListenerMethods = new HashSet<>();
199198
skipListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnSkipInRead.class));
200199
skipListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnSkipInProcess.class));
201200
skipListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnSkipInWrite.class));
202201

203202
if (!skipListenerMethods.isEmpty()) {
203+
listenerErrors.clear();
204204
StepListenerFactoryBean factory = new StepListenerFactoryBean();
205205
factory.setDelegate(listener);
206206
skipListeners.add((SkipListener<I, O>) factory.getObject());
207207
}
208+
else if (!listenerErrors.isEmpty()) {
209+
listenerErrors.add(new IllegalArgumentException(
210+
"Missing @OnSkipInRead, @OnSkipInProcess or @OnSkipInWrite annotations on Listener."));
211+
}
208212

209213
return this;
210214
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FlowStepBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ public Step build() {
6060
step.setName(getName());
6161
step.setFlow(flow);
6262
super.enhance(step);
63+
if (!listenerErrors.isEmpty()) {
64+
throw new StepBuilderException(
65+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
66+
}
6367
try {
6468
step.afterPropertiesSet();
6569
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/JobStepBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public Step build() {
8686
JobStep step = new JobStep();
8787
step.setName(getName());
8888
super.enhance(step);
89+
if (!listenerErrors.isEmpty()) {
90+
throw new StepBuilderException(
91+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
92+
}
8993
if (job != null) {
9094
step.setJob(job);
9195
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/PartitionStepBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ public Step build() {
163163
step.setName(getName());
164164
super.enhance(step);
165165

166+
if (!listenerErrors.isEmpty()) {
167+
throw new StepBuilderException(
168+
new IllegalArgumentException("Errors occurred while registering listeners" + listenerErrors));
169+
}
170+
166171
if (partitionHandler != null) {
167172
step.setPartitionHandler(partitionHandler);
168173
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,15 @@ public SimpleStepBuilder<I, O> listener(Object listener) {
270270
itemListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnWriteError.class));
271271

272272
if (!itemListenerMethods.isEmpty()) {
273+
listenerErrors.clear();
273274
StepListenerFactoryBean factory = new StepListenerFactoryBean();
274275
factory.setDelegate(listener);
275276
itemListeners.add((StepListener) factory.getObject());
276277
}
278+
else if (!listenerErrors.isEmpty()) {
279+
listenerErrors.add(new IllegalArgumentException(
280+
"Missing @BeforeRead, @AfterRead, @BeforeProcess, @AfterProcess, @BeforeWrite, @AfterWrite, @OnReadError, @OnProcessError or @OnWriteError annotations on Listener."));
281+
}
277282

278283
return this;
279284
}

spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/StepBuilderHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public abstract class StepBuilderHelper<B extends StepBuilderHelper<B>> {
5353

5454
protected final CommonStepProperties properties;
5555

56+
protected List<Throwable> listenerErrors = new ArrayList<>();
57+
5658
/**
5759
* Create a new {@link StepBuilderHelper} with the given job repository.
5860
* @param jobRepository the job repository
@@ -82,6 +84,7 @@ public StepBuilderHelper(String name, JobRepository jobRepository) {
8284
*/
8385
protected StepBuilderHelper(StepBuilderHelper<?> parent) {
8486
this.properties = new CommonStepProperties(parent.properties);
87+
this.listenerErrors = parent.listenerErrors;
8588
}
8689

8790
/**
@@ -125,6 +128,10 @@ public B listener(Object listener) {
125128
factory.setDelegate(listener);
126129
properties.addStepExecutionListener((StepExecutionListener) factory.getObject());
127130
}
131+
else {
132+
listenerErrors
133+
.add(new IllegalArgumentException("Missing @BeforeStep or @AfterStep annotations on Listener."));
134+
}
128135

129136
return self();
130137
}

spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.junit.jupiter.api.Test;
2121

22+
import org.mockito.Mockito;
2223
import org.springframework.batch.core.ExitStatus;
2324
import org.springframework.batch.core.job.Job;
2425
import org.springframework.batch.core.job.JobExecution;
@@ -40,6 +41,7 @@
4041
import org.springframework.transaction.PlatformTransactionManager;
4142

4243
import static org.junit.jupiter.api.Assertions.assertEquals;
44+
import static org.junit.jupiter.api.Assertions.assertThrows;
4345

4446
/**
4547
* @author Mahmoud Ben Hassine
@@ -65,6 +67,16 @@ void testListeners() throws Exception {
6567

6668
}
6769

70+
@Test
71+
void testInvalidListener() {
72+
assertThrows(JobBuilderException.class,
73+
() -> new JobBuilder("job", Mockito.mock()).listener(new InvalidListener())
74+
.start(new StepBuilder("step", Mockito.mock())
75+
.tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED, Mockito.mock())
76+
.build())
77+
.build());
78+
}
79+
6880
@Configuration
6981
@EnableBatchProcessing
7082
static class MyJobConfiguration {
@@ -130,4 +142,14 @@ public void afterJob(JobExecution jobExecution) {
130142

131143
}
132144

145+
public static class InvalidListener {
146+
147+
public void beforeStep() {
148+
}
149+
150+
public void afterStep() {
151+
}
152+
153+
}
154+
133155
}

spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/StepBuilderTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.function.UnaryOperator;
2121

2222
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Disabled;
2324
import org.junit.jupiter.api.Test;
2425

2526
import org.springframework.batch.core.BatchStatus;
@@ -60,6 +61,7 @@
6061
import org.springframework.transaction.PlatformTransactionManager;
6162

6263
import static org.junit.jupiter.api.Assertions.assertEquals;
64+
import static org.junit.jupiter.api.Assertions.assertThrows;
6365

6466
/**
6567
* @author Dave Syer
@@ -117,6 +119,14 @@ void testListeners() throws Exception {
117119
assertEquals(1, AnnotationBasedStepExecutionListener.afterChunkCount);
118120
}
119121

122+
@Test
123+
void testMissingAnnotationsForListeners() {
124+
assertThrows(StepBuilderException.class,
125+
() -> new StepBuilder("step", jobRepository).listener(new InvalidListener())
126+
.tasklet((contribution, chunkContext) -> null, transactionManager)
127+
.build());
128+
}
129+
120130
@Test
121131
void testAnnotationBasedChunkListenerForTaskletStep() throws Exception {
122132
TaskletStepBuilder builder = new StepBuilder("step", jobRepository)
@@ -157,6 +167,7 @@ void testAnnotationBasedChunkListenerForFaultTolerantTaskletStep() throws Except
157167
assertEquals(1, AnnotationBasedChunkListener.afterChunkCount);
158168
}
159169

170+
@Disabled
160171
@Test
161172
void testAnnotationBasedChunkListenerForJobStepBuilder() throws Exception {
162173
SimpleJob job = new SimpleJob("job");
@@ -465,4 +476,14 @@ public void afterChunkError() {
465476

466477
}
467478

479+
public static class InvalidListener {
480+
481+
public void beforeStep() {
482+
}
483+
484+
public void afterStep() {
485+
}
486+
487+
}
488+
468489
}

spring-batch-integration/src/test/java/org/springframework/batch/integration/chunk/RemoteChunkingManagerStepBuilderTests.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
import org.junit.jupiter.api.Test;
2525

26+
import org.springframework.batch.core.annotation.AfterChunk;
27+
import org.springframework.batch.core.annotation.AfterChunkError;
28+
import org.springframework.batch.core.annotation.BeforeChunk;
2629
import org.springframework.batch.core.listener.ChunkListener;
2730
import org.springframework.batch.core.listener.ItemReadListener;
2831
import org.springframework.batch.core.listener.ItemWriteListener;
@@ -225,7 +228,7 @@ void testSetters() throws Exception {
225228
// when
226229
DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute();
227230

228-
Object annotatedListener = new Object();
231+
Object annotatedListener = new AnnotationBasedChunkListener();
229232
MapRetryContextCache retryCache = new MapRetryContextCache();
230233
RepeatTemplate stepOperations = new RepeatTemplate();
231234
NoBackOffPolicy backOffPolicy = new NoBackOffPolicy();
@@ -372,4 +375,20 @@ JdbcTransactionManager transactionManager(DataSource dataSource) {
372375

373376
}
374377

378+
public static class AnnotationBasedChunkListener {
379+
380+
@BeforeChunk
381+
public void beforeChunk() {
382+
}
383+
384+
@AfterChunk
385+
public void afterChunk() {
386+
}
387+
388+
@AfterChunkError
389+
public void afterChunkError() {
390+
}
391+
392+
}
393+
375394
}

0 commit comments

Comments
 (0)