Skip to content

Commit de9770a

Browse files
committed
Add afterJobSaved method and @AfterJobSaved interface (#4851)
Signed-off-by: Hannes Oswald <65758037+hannosgit@users.noreply.github.com>
1 parent 08c4cb1 commit de9770a

File tree

10 files changed

+121
-2
lines changed

10 files changed

+121
-2
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/JobExecutionListener.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,14 @@ default void beforeJob(JobExecution jobExecution) {
4242
default void afterJob(JobExecution jobExecution) {
4343
}
4444

45+
/**
46+
* Callback after completion of a job and job metadata is saved.
47+
* Called after both successful and failed executions.
48+
* In contrast to {@link JobExecutionListener#afterJob(JobExecution)},
49+
* it is not possible to change the given jobExecution as it was already saved.
50+
* @param jobExecution the current {@link JobExecution}
51+
*/
52+
default void afterJobSaved(JobExecution jobExecution) {
53+
}
54+
4555
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2006-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.core.annotation;
18+
19+
import org.springframework.batch.core.Job;
20+
import org.springframework.batch.core.JobExecution;
21+
import org.springframework.batch.core.JobExecutionListener;
22+
23+
import java.lang.annotation.ElementType;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
/**
29+
* Marks a method to be called after a {@link Job} has completed and the job metadata is saved.
30+
* Annotated methods are called regardless of the status of the {@link JobExecution}. <br>
31+
* <br>
32+
* Expected signature: void afterJobSaved({@link JobExecution} jobExecution)
33+
*
34+
* @see JobExecutionListener
35+
*/
36+
@Retention(RetentionPolicy.RUNTIME)
37+
@Target({ ElementType.METHOD })
38+
public @interface AfterJobSaved {
39+
40+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ public final void execute(JobExecution execution) {
364364
}
365365

366366
jobRepository.update(execution);
367+
368+
try {
369+
listener.afterJobSaved(execution);
370+
}
371+
catch (Exception e) {
372+
logger.error("Exception encountered in afterJobSaved callback", e);
373+
}
367374
}
368375
finally {
369376
JobSynchronizationManager.release();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.batch.core.JobParametersIncrementer;
3232
import org.springframework.batch.core.JobParametersValidator;
3333
import org.springframework.batch.core.annotation.AfterJob;
34+
import org.springframework.batch.core.annotation.AfterJobSaved;
3435
import org.springframework.batch.core.annotation.BeforeJob;
3536
import org.springframework.batch.core.job.AbstractJob;
3637
import org.springframework.batch.core.listener.JobListenerFactoryBean;
@@ -145,6 +146,7 @@ public B listener(Object listener) {
145146
Set<Method> jobExecutionListenerMethods = new HashSet<>();
146147
jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeJob.class));
147148
jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterJob.class));
149+
jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterJobSaved.class));
148150

149151
if (jobExecutionListenerMethods.size() > 0) {
150152
JobListenerFactoryBean factory = new JobListenerFactoryBean();

spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeJobExecutionListener.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,17 @@ public void beforeJob(JobExecution jobExecution) {
7474
}
7575
}
7676

77+
/**
78+
* Call the registered listeners in reverse order, respecting and prioritising those
79+
* that implement {@link Ordered}.
80+
* @see org.springframework.batch.core.JobExecutionListener#afterJobSaved(org.springframework.batch.core.JobExecution)
81+
*/
82+
@Override
83+
public void afterJobSaved(JobExecution jobExecution) {
84+
for (Iterator<JobExecutionListener> iterator = listeners.reverse(); iterator.hasNext();) {
85+
JobExecutionListener listener = iterator.next();
86+
listener.afterJobSaved(jobExecution);
87+
}
88+
}
89+
7790
}

spring-batch-core/src/main/java/org/springframework/batch/core/listener/JobListenerMetaData.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.batch.core.JobExecution;
2323
import org.springframework.batch.core.JobExecutionListener;
2424
import org.springframework.batch.core.annotation.AfterJob;
25+
import org.springframework.batch.core.annotation.AfterJobSaved;
2526
import org.springframework.batch.core.annotation.BeforeJob;
2627
import org.springframework.lang.Nullable;
2728

@@ -37,7 +38,8 @@
3738
public enum JobListenerMetaData implements ListenerMetaData {
3839

3940
BEFORE_JOB("beforeJob", "before-job-method", BeforeJob.class),
40-
AFTER_JOB("afterJob", "after-job-method", AfterJob.class);
41+
AFTER_JOB("afterJob", "after-job-method", AfterJob.class),
42+
AFTER_JOB_SAVED("afterJobSaved", "after-job-saved-method", AfterJobSaved .class);
4143

4244
private final String methodName;
4345

spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobExecutionListenerParserTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.batch.core.JobExecution;
2323
import org.springframework.batch.core.JobParametersBuilder;
2424
import org.springframework.batch.core.annotation.AfterJob;
25+
import org.springframework.batch.core.annotation.AfterJobSaved;
2526
import org.springframework.batch.core.annotation.BeforeJob;
2627
import org.springframework.batch.core.repository.JobRepository;
2728
import org.springframework.beans.factory.annotation.Autowired;
@@ -38,6 +39,8 @@ public class JobExecutionListenerParserTests {
3839

3940
public static boolean afterCalled = false;
4041

42+
public static boolean afterSavedCalled = false;
43+
4144
@Autowired
4245
Job job;
4346

@@ -51,6 +54,7 @@ void testListeners() throws Exception {
5154
job.execute(jobExecution);
5255
assertTrue(beforeCalled);
5356
assertTrue(afterCalled);
57+
assertTrue(afterSavedCalled);
5458
}
5559

5660
public static class TestComponent {
@@ -65,6 +69,11 @@ public void after() {
6569
afterCalled = true;
6670
}
6771

72+
@AfterJobSaved
73+
public void afterJobSaved() {
74+
afterSavedCalled = true;
75+
}
76+
6877
}
6978

7079
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.batch.core.JobExecutionListener;
2626
import org.springframework.batch.core.JobParameters;
2727
import org.springframework.batch.core.annotation.AfterJob;
28+
import org.springframework.batch.core.annotation.AfterJobSaved;
2829
import org.springframework.batch.core.annotation.BeforeJob;
2930
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
3031
import org.springframework.batch.core.launch.JobLauncher;
@@ -60,9 +61,10 @@ void testListeners() throws Exception {
6061
assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
6162
assertEquals(1, AnnotationBasedJobExecutionListener.beforeJobCount);
6263
assertEquals(1, AnnotationBasedJobExecutionListener.afterJobCount);
64+
assertEquals(1, AnnotationBasedJobExecutionListener.afterJobSavedCount);
6365
assertEquals(1, InterfaceBasedJobExecutionListener.beforeJobCount);
6466
assertEquals(1, InterfaceBasedJobExecutionListener.afterJobCount);
65-
67+
assertEquals(1, InterfaceBasedJobExecutionListener.afterJobSavedCount);
6668
}
6769

6870
@Configuration
@@ -100,6 +102,8 @@ static class InterfaceBasedJobExecutionListener implements JobExecutionListener
100102

101103
public static int afterJobCount = 0;
102104

105+
public static int afterJobSavedCount = 0;
106+
103107
@Override
104108
public void beforeJob(JobExecution jobExecution) {
105109
beforeJobCount++;
@@ -110,6 +114,10 @@ public void afterJob(JobExecution jobExecution) {
110114
afterJobCount++;
111115
}
112116

117+
@Override
118+
public void afterJobSaved(JobExecution jobExecution) {
119+
afterJobSavedCount++;
120+
}
113121
}
114122

115123
static class AnnotationBasedJobExecutionListener {
@@ -118,6 +126,8 @@ static class AnnotationBasedJobExecutionListener {
118126

119127
public static int afterJobCount = 0;
120128

129+
public static int afterJobSavedCount = 0;
130+
121131
@BeforeJob
122132
public void beforeJob(JobExecution jobExecution) {
123133
beforeJobCount++;
@@ -128,6 +138,11 @@ public void afterJob(JobExecution jobExecution) {
128138
afterJobCount++;
129139
}
130140

141+
@AfterJobSaved
142+
public void afterJobSaved() {
143+
afterJobSavedCount++;
144+
}
145+
131146
}
132147

133148
}

spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeJobExecutionListenerTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,16 @@ public void beforeJob(JobExecution stepExecution) {
7878
assertEquals(1, list.size());
7979
}
8080

81+
@Test
82+
void testAfterJobSaved() {
83+
listener.register(new JobExecutionListener() {
84+
@Override
85+
public void afterJobSaved(JobExecution jobExecution) {
86+
list.add("foo");
87+
}
88+
});
89+
listener.afterJobSaved(new JobExecution(new JobInstance(11L, "testOpenJob"), null));
90+
assertEquals(1, list.size());
91+
}
92+
8193
}

spring-batch-core/src/test/java/org/springframework/batch/core/listener/JobListenerFactoryBeanTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ void testWithInterface() {
5757
JobExecution jobExecution = new JobExecution(11L);
5858
listener.beforeJob(jobExecution);
5959
listener.afterJob(jobExecution);
60+
listener.afterJobSaved(jobExecution);
6061
assertTrue(delegate.beforeJobCalled);
6162
assertTrue(delegate.afterJobCalled);
63+
assertTrue(delegate.afterJobSavedCalled);
6264
}
6365

6466
@Test
@@ -240,6 +242,8 @@ private static class JobListenerWithInterface implements JobExecutionListener {
240242

241243
boolean afterJobCalled = false;
242244

245+
boolean afterJobSavedCalled = false;
246+
243247
@Override
244248
public void afterJob(JobExecution jobExecution) {
245249
afterJobCalled = true;
@@ -250,6 +254,11 @@ public void beforeJob(JobExecution jobExecution) {
250254
beforeJobCalled = true;
251255
}
252256

257+
@Override
258+
public void afterJobSaved(JobExecution jobExecution) {
259+
afterJobSavedCalled = true;
260+
}
261+
253262
}
254263

255264
private static class AnnotatedTestClass {

0 commit comments

Comments
 (0)