diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecutionListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecutionListener.java index bd0c7b6a92..5ded8efb70 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecutionListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobExecutionListener.java @@ -42,4 +42,14 @@ default void beforeJob(JobExecution jobExecution) { default void afterJob(JobExecution jobExecution) { } + /** + * Callback after completion of a job and job metadata is saved. + * Called after both successful and failed executions. + * In contrast to {@link JobExecutionListener#afterJob(JobExecution)}, + * it is not possible to change the given jobExecution as it was already saved. + * @param jobExecution the current {@link JobExecution} + */ + default void afterJobSaved(JobExecution jobExecution) { + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterJobSaved.java b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterJobSaved.java new file mode 100644 index 0000000000..712fb008a0 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/AfterJobSaved.java @@ -0,0 +1,40 @@ +/* + * Copyright 2006-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.core.annotation; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method to be called after a {@link Job} has completed and the job metadata is saved. + * Annotated methods are called regardless of the status of the {@link JobExecution}.
+ *
+ * Expected signature: void afterJobSaved({@link JobExecution} jobExecution) + * + * @see JobExecutionListener + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface AfterJobSaved { + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java index 34d6d19f58..7fbf989815 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java @@ -364,6 +364,13 @@ public final void execute(JobExecution execution) { } jobRepository.update(execution); + + try { + listener.afterJobSaved(execution); + } + catch (Exception e) { + logger.error("Exception encountered in afterJobSaved callback", e); + } } finally { JobSynchronizationManager.release(); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/JobBuilderHelper.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/JobBuilderHelper.java index dee585863e..9e2d500e6f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/JobBuilderHelper.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/JobBuilderHelper.java @@ -31,6 +31,7 @@ import org.springframework.batch.core.JobParametersIncrementer; import org.springframework.batch.core.JobParametersValidator; import org.springframework.batch.core.annotation.AfterJob; +import org.springframework.batch.core.annotation.AfterJobSaved; import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.listener.JobListenerFactoryBean; @@ -145,6 +146,7 @@ public B listener(Object listener) { Set jobExecutionListenerMethods = new HashSet<>(); jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeJob.class)); jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterJob.class)); + jobExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterJobSaved.class)); if (jobExecutionListenerMethods.size() > 0) { JobListenerFactoryBean factory = new JobListenerFactoryBean(); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeJobExecutionListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeJobExecutionListener.java index 304b1b2a92..cba705fa58 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeJobExecutionListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeJobExecutionListener.java @@ -74,4 +74,17 @@ public void beforeJob(JobExecution jobExecution) { } } + /** + * Call the registered listeners in reverse order, respecting and prioritising those + * that implement {@link Ordered}. + * @see org.springframework.batch.core.JobExecutionListener#afterJobSaved(org.springframework.batch.core.JobExecution) + */ + @Override + public void afterJobSaved(JobExecution jobExecution) { + for (Iterator iterator = listeners.reverse(); iterator.hasNext();) { + JobExecutionListener listener = iterator.next(); + listener.afterJobSaved(jobExecution); + } + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/JobListenerMetaData.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/JobListenerMetaData.java index 3f5b515502..6b901c1c39 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/JobListenerMetaData.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/JobListenerMetaData.java @@ -22,6 +22,7 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.annotation.AfterJob; +import org.springframework.batch.core.annotation.AfterJobSaved; import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.lang.Nullable; @@ -37,7 +38,8 @@ public enum JobListenerMetaData implements ListenerMetaData { BEFORE_JOB("beforeJob", "before-job-method", BeforeJob.class), - AFTER_JOB("afterJob", "after-job-method", AfterJob.class); + AFTER_JOB("afterJob", "after-job-method", AfterJob.class), + AFTER_JOB_SAVED("afterJobSaved", "after-job-saved-method", AfterJobSaved .class); private final String methodName; diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobExecutionListenerParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobExecutionListenerParserTests.java index 8f9d3a195d..e108d72c43 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobExecutionListenerParserTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/JobExecutionListenerParserTests.java @@ -22,6 +22,7 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.annotation.AfterJob; +import org.springframework.batch.core.annotation.AfterJobSaved; import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,8 @@ public class JobExecutionListenerParserTests { public static boolean afterCalled = false; + public static boolean afterSavedCalled = false; + @Autowired Job job; @@ -51,6 +54,7 @@ void testListeners() throws Exception { job.execute(jobExecution); assertTrue(beforeCalled); assertTrue(afterCalled); + assertTrue(afterSavedCalled); } public static class TestComponent { @@ -65,6 +69,11 @@ public void after() { afterCalled = true; } + @AfterJobSaved + public void afterJobSaved() { + afterSavedCalled = true; + } + } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java index 9678a8e4cc..81d5ba2a3a 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java @@ -25,6 +25,7 @@ import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.annotation.AfterJob; +import org.springframework.batch.core.annotation.AfterJobSaved; import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.launch.JobLauncher; @@ -60,9 +61,10 @@ void testListeners() throws Exception { assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); assertEquals(1, AnnotationBasedJobExecutionListener.beforeJobCount); assertEquals(1, AnnotationBasedJobExecutionListener.afterJobCount); + assertEquals(1, AnnotationBasedJobExecutionListener.afterJobSavedCount); assertEquals(1, InterfaceBasedJobExecutionListener.beforeJobCount); assertEquals(1, InterfaceBasedJobExecutionListener.afterJobCount); - + assertEquals(1, InterfaceBasedJobExecutionListener.afterJobSavedCount); } @Configuration @@ -100,6 +102,8 @@ static class InterfaceBasedJobExecutionListener implements JobExecutionListener public static int afterJobCount = 0; + public static int afterJobSavedCount = 0; + @Override public void beforeJob(JobExecution jobExecution) { beforeJobCount++; @@ -110,6 +114,10 @@ public void afterJob(JobExecution jobExecution) { afterJobCount++; } + @Override + public void afterJobSaved(JobExecution jobExecution) { + afterJobSavedCount++; + } } static class AnnotationBasedJobExecutionListener { @@ -118,6 +126,8 @@ static class AnnotationBasedJobExecutionListener { public static int afterJobCount = 0; + public static int afterJobSavedCount = 0; + @BeforeJob public void beforeJob(JobExecution jobExecution) { beforeJobCount++; @@ -128,6 +138,11 @@ public void afterJob(JobExecution jobExecution) { afterJobCount++; } + @AfterJobSaved + public void afterJobSaved() { + afterJobSavedCount++; + } + } } \ No newline at end of file diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeJobExecutionListenerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeJobExecutionListenerTests.java index 0ee3e43c1a..867754daec 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeJobExecutionListenerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/CompositeJobExecutionListenerTests.java @@ -78,4 +78,16 @@ public void beforeJob(JobExecution stepExecution) { assertEquals(1, list.size()); } + @Test + void testAfterJobSaved() { + listener.register(new JobExecutionListener() { + @Override + public void afterJobSaved(JobExecution jobExecution) { + list.add("foo"); + } + }); + listener.afterJobSaved(new JobExecution(new JobInstance(11L, "testOpenJob"), null)); + assertEquals(1, list.size()); + } + } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/JobListenerFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/JobListenerFactoryBeanTests.java index 59bdb90e40..5230ad7e93 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/JobListenerFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/JobListenerFactoryBeanTests.java @@ -57,8 +57,10 @@ void testWithInterface() { JobExecution jobExecution = new JobExecution(11L); listener.beforeJob(jobExecution); listener.afterJob(jobExecution); + listener.afterJobSaved(jobExecution); assertTrue(delegate.beforeJobCalled); assertTrue(delegate.afterJobCalled); + assertTrue(delegate.afterJobSavedCalled); } @Test @@ -240,6 +242,8 @@ private static class JobListenerWithInterface implements JobExecutionListener { boolean afterJobCalled = false; + boolean afterJobSavedCalled = false; + @Override public void afterJob(JobExecution jobExecution) { afterJobCalled = true; @@ -250,6 +254,11 @@ public void beforeJob(JobExecution jobExecution) { beforeJobCalled = true; } + @Override + public void afterJobSaved(JobExecution jobExecution) { + afterJobSavedCalled = true; + } + } private static class AnnotatedTestClass {