{
22 |
23 | /**
24 | * @deprecated Accessing {@link org.springframework.batch.core.Job} properties via this singleton is not safe for
25 | * asynchronously executing the same Job
multiple times with different properties. In this case a {@link JobExecution}
26 | * may incorrectly use the properties of another, concurrently running JobExecution
of the same Job
.
27 | *
28 | *
29 | * Instead, it is recommended to access job properties via either {@link StepExecution#getJobParameters()} or by annotating your
30 | * Spring-wired job beans with @Value("#{jobParameters['key']}")
. You can get a handle of a StepExecution
31 | * by implementing {@link org.springframework.batch.core.StepExecutionListener} or extending
32 | * {@link com.github.chrisgleissner.springbatchrest.util.core.tasklet.StepExecutionListenerTasklet}.
33 | *
34 | *
35 | * For convenience, when using
36 | * {@link com.github.chrisgleissner.springbatchrest.util.core.JobBuilder#createJob(String, Consumer)} to build a job,
37 | * the returned {@link PropertyResolver} will first resolve against job properties, then against Spring properties.
38 | *
39 | *
40 | * @see com.github.chrisgleissner.springbatchrest.util.core.JobBuilder#createJob(String, Consumer)
41 | */
42 | @Deprecated
43 | public static JobPropertyResolvers JobProperties;
44 |
45 | private Environment environment;
46 | private Map resolvers = new ConcurrentHashMap<>();
47 |
48 | @Autowired
49 | public JobPropertyResolvers(Environment environment, JobExecutionAspect jobExecutionAspect) {
50 | this.environment = environment;
51 | jobExecutionAspect.register(this);
52 | JobProperties = this;
53 | }
54 |
55 | public PropertyResolver of(String jobName) {
56 | JobPropertyResolver jobPropertyResolver = resolvers.get(jobName);
57 | return jobPropertyResolver == null ? environment : jobPropertyResolver;
58 | }
59 |
60 | public void started(JobConfig jobConfig) {
61 | String jobName = jobConfig.getName();
62 | JobPropertyResolver resolver = new JobPropertyResolver(jobConfig, environment);
63 | resolvers.put(jobName, resolver);
64 | log.info("Enabled {}", resolver);
65 | }
66 |
67 | @Override
68 | public void accept(JobExecution je) {
69 | if (!je.isRunning()) {
70 | JobPropertyResolver resolver = resolvers.remove(je.getJobInstance().getJobName());
71 | if (resolver != null)
72 | log.info("Disabled {}", je.getJobInstance().getJobName());
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/core/tasklet/PropertyResolverConsumerTasklet.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core.tasklet;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import org.springframework.batch.core.ExitStatus;
5 | import org.springframework.batch.core.Job;
6 | import org.springframework.batch.core.StepContribution;
7 | import org.springframework.batch.core.StepExecution;
8 | import org.springframework.batch.core.StepExecutionListener;
9 | import org.springframework.batch.core.launch.support.RunIdIncrementer;
10 | import org.springframework.batch.core.scope.context.ChunkContext;
11 | import org.springframework.batch.core.step.tasklet.Tasklet;
12 | import org.springframework.batch.repeat.RepeatStatus;
13 | import org.springframework.core.env.AbstractEnvironment;
14 | import org.springframework.core.env.Environment;
15 | import org.springframework.core.env.MutablePropertySources;
16 | import org.springframework.core.env.PropertiesPropertySource;
17 | import org.springframework.core.env.PropertyResolver;
18 | import org.springframework.core.env.PropertySources;
19 | import org.springframework.core.env.PropertySourcesPropertyResolver;
20 |
21 | import java.util.Optional;
22 | import java.util.Properties;
23 | import java.util.function.Consumer;
24 |
25 | import static org.springframework.batch.repeat.RepeatStatus.FINISHED;
26 |
27 | @RequiredArgsConstructor
28 | public class PropertyResolverConsumerTasklet implements Tasklet, StepExecutionListener {
29 | private final Environment environment;
30 | private final Consumer propertyResolverConsumer;
31 | private StepExecution stepExecution;
32 |
33 | @Override
34 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
35 | propertyResolverConsumer.accept(new PropertySourcesPropertyResolver(propertySources(
36 | stepExecution.getJobExecution().getJobConfigurationName(),
37 | stepExecution.getJobParameters().toProperties(), environment)));
38 | return FINISHED;
39 | }
40 |
41 | private PropertySources propertySources(String propertyName, Properties properties, Environment env) {
42 | MutablePropertySources propertySources = new MutablePropertySources();
43 | if (properties != null)
44 | propertySources.addFirst(new PropertiesPropertySource(Optional.ofNullable(propertyName).orElse("jobConfig"), properties));
45 | ((AbstractEnvironment) env).getPropertySources().forEach(propertySources::addLast);
46 | return propertySources;
47 | }
48 |
49 | @Override public void beforeStep(StepExecution stepExecution) {
50 | this.stepExecution = stepExecution;
51 | }
52 |
53 | @Override public ExitStatus afterStep(StepExecution stepExecution) {
54 | return null;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/core/tasklet/RunnableTasklet.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core.tasklet;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import org.springframework.batch.core.StepContribution;
5 | import org.springframework.batch.core.scope.context.ChunkContext;
6 | import org.springframework.batch.core.step.tasklet.Tasklet;
7 | import org.springframework.batch.repeat.RepeatStatus;
8 |
9 | import static org.springframework.batch.repeat.RepeatStatus.FINISHED;
10 |
11 | @RequiredArgsConstructor
12 | public class RunnableTasklet implements Tasklet {
13 | private final Runnable runnable;
14 |
15 | @Override
16 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
17 | runnable.run();
18 | return FINISHED;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/core/tasklet/StepExecutionListenerTasklet.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core.tasklet;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import org.springframework.batch.core.ExitStatus;
5 | import org.springframework.batch.core.StepContribution;
6 | import org.springframework.batch.core.StepExecution;
7 | import org.springframework.batch.core.StepExecutionListener;
8 | import org.springframework.batch.core.scope.context.ChunkContext;
9 | import org.springframework.batch.core.step.tasklet.Tasklet;
10 | import org.springframework.batch.repeat.RepeatStatus;
11 | import org.springframework.core.env.AbstractEnvironment;
12 | import org.springframework.core.env.Environment;
13 | import org.springframework.core.env.MutablePropertySources;
14 | import org.springframework.core.env.PropertiesPropertySource;
15 | import org.springframework.core.env.PropertyResolver;
16 | import org.springframework.core.env.PropertySources;
17 | import org.springframework.core.env.PropertySourcesPropertyResolver;
18 |
19 | import java.util.Optional;
20 | import java.util.Properties;
21 | import java.util.function.Consumer;
22 |
23 | import static org.springframework.batch.repeat.RepeatStatus.FINISHED;
24 |
25 | @RequiredArgsConstructor
26 | public class StepExecutionListenerTasklet implements Tasklet, StepExecutionListener {
27 | private final Consumer stepExecutionConsumer;
28 | private StepExecution stepExecution;
29 |
30 | @Override
31 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
32 | stepExecutionConsumer.accept(stepExecution);
33 | return FINISHED;
34 | }
35 |
36 | @Override public void beforeStep(StepExecution stepExecution) {
37 | this.stepExecution = stepExecution;
38 | }
39 |
40 | @Override public ExitStatus afterStep(StepExecution stepExecution) {
41 | return null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/quartz/AdHocScheduler.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz;
2 |
3 | import com.github.chrisgleissner.springbatchrest.util.TriggerUtil;
4 | import com.github.chrisgleissner.springbatchrest.util.core.JobBuilder;
5 | import com.github.chrisgleissner.springbatchrest.util.core.JobConfig;
6 | import com.github.chrisgleissner.springbatchrest.util.core.JobParamsDetail;
7 |
8 | import lombok.extern.slf4j.Slf4j;
9 | import org.quartz.JobDetail;
10 | import org.quartz.Scheduler;
11 | import org.quartz.Trigger;
12 | import org.springframework.batch.core.Job;
13 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
14 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
15 | import org.springframework.beans.factory.annotation.Autowired;
16 | import org.springframework.stereotype.Component;
17 |
18 | import static java.lang.String.format;
19 | import static org.quartz.JobBuilder.newJob;
20 |
21 | import java.util.Date;
22 |
23 | /**
24 | * Allows to schedule Spring Batch jobs via Quartz by using a
25 | * {@link #schedule(String, Job, String)} method rather than Spring wiring each
26 | * job. This allows for programmatic creation of multiple jobs at run-time.
27 | */
28 | @Slf4j
29 | @Component
30 | public class AdHocScheduler {
31 |
32 | private final JobBuilder jobBuilder;
33 | private Scheduler scheduler;
34 | private JobBuilderFactory jobBuilderFactory;
35 | private StepBuilderFactory stepBuilderFactory;
36 |
37 | @Autowired
38 | public AdHocScheduler(JobBuilder jobBuilder, Scheduler scheduler, JobBuilderFactory jobBuilderFactory,
39 | StepBuilderFactory stepBuilderFactory) {
40 | this.jobBuilder = jobBuilder;
41 | this.scheduler = scheduler;
42 | this.jobBuilderFactory = jobBuilderFactory;
43 | this.stepBuilderFactory = stepBuilderFactory;
44 | }
45 |
46 | /**
47 | * Schedules a Spring Batch job via a future Date. Job referenced via
48 | * jobConfig's name must be a valid registered bean name for a Job object.
49 | */
50 | public synchronized void schedule(JobConfig jobConfig, Date dateToRun) {
51 | log.debug("Scheduling job {} with custom Trigger", jobConfig.getName());
52 | try {
53 | JobDetail jobDetail = this.jobDetailFor(jobConfig);
54 | Trigger trigger = TriggerUtil.triggerFor(dateToRun, jobConfig.getName());
55 | scheduler.unscheduleJob(trigger.getKey());
56 | scheduler.scheduleJob(jobDetail, trigger);
57 | log.info("Scheduled job {} with Date {}", jobConfig.getName(), dateToRun.toString());
58 | } catch (Exception e) {
59 | throw new RuntimeException(
60 | format("Can't schedule job %s with date: %s", jobConfig.getName(), dateToRun.toString()), e);
61 | }
62 | }
63 |
64 | /**
65 | * Schedules a Spring Batch job via a Quartz cron expression. Uses the job name
66 | * of the provided job.
67 | */
68 | public synchronized Job schedule(Job job, String cronExpression) {
69 | return this.schedule(job.getName(), job, cronExpression);
70 | }
71 |
72 | /**
73 | * Schedules a Spring Batch job via a Quartz cron expression. Also registers the
74 | * job with the specified jobName, rather than the job param's name
75 | */
76 | public synchronized Job schedule(String jobName, Job job, String cronExpression) {
77 | log.debug("Scheduling job {} with CRON expression {}", jobName, cronExpression);
78 | try {
79 | jobBuilder.registerJob(job);
80 | JobDetail jobDetail = this.jobDetailFor(jobName);
81 |
82 | Trigger trigger = TriggerUtil.triggerFor(cronExpression, jobName);
83 |
84 | scheduler.unscheduleJob(trigger.getKey());
85 | scheduler.scheduleJob(jobDetail, trigger);
86 | log.info("Scheduled job {} with CRON expression {}", jobName, cronExpression);
87 | } catch (Exception e) {
88 | throw new RuntimeException(format("Can't schedule job %s with cronExpression %s", jobName, cronExpression),
89 | e);
90 | }
91 | return job;
92 | }
93 |
94 | /**
95 | * Schedules a Spring Batch job via a Quartz cron expression. Job referenced via
96 | * jobConfig's name must be a valid registered bean name for a Job object.
97 | */
98 | public synchronized void schedule(JobConfig jobConfig, String cronExpression) {
99 | log.debug("Scheduling job {} with CRON expression {}", jobConfig.getName(), cronExpression);
100 | try {
101 | JobDetail jobDetail = this.jobDetailFor(jobConfig);
102 |
103 | Trigger trigger = TriggerUtil.triggerFor(cronExpression, jobConfig.getName());
104 |
105 | scheduler.unscheduleJob(trigger.getKey());
106 | scheduler.scheduleJob(jobDetail, trigger);
107 | log.info("Scheduled job {} with CRON expression {}", jobConfig.getName(), cronExpression);
108 | } catch (Exception e) {
109 | throw new RuntimeException(
110 | format("Can't schedule job %s with cronExpression %s", jobConfig.getName(), cronExpression), e);
111 | }
112 | }
113 |
114 | /**
115 | * Starts the Quartz scheduler unless it is already started. Necessary for any
116 | * scheduled jobs to start.
117 | */
118 | public synchronized void start() {
119 | try {
120 | if (!scheduler.isStarted()) {
121 | scheduler.start();
122 | log.info("Started Quartz scheduler");
123 | } else {
124 | log.warn("Quartz scheduler already started");
125 | }
126 | } catch (Exception e) {
127 | throw new RuntimeException("Could not start Quartz scheduler", e);
128 | }
129 | }
130 |
131 | public synchronized void pause() {
132 | try {
133 | if (scheduler.isStarted() && !scheduler.isInStandbyMode()) {
134 | scheduler.pauseAll();
135 | log.info("Paused Quartz scheduler");
136 | }
137 | } catch (Exception e) {
138 | throw new RuntimeException("Could not pause Quartz scheduler", e);
139 | }
140 | }
141 |
142 | public synchronized void resume() {
143 | try {
144 | if (scheduler.isStarted() && scheduler.isInStandbyMode()) {
145 | scheduler.resumeAll();
146 | log.info("Resumed Quartz scheduler");
147 | }
148 | } catch (Exception e) {
149 | throw new RuntimeException("Could not resumse Quartz scheduler", e);
150 | }
151 | }
152 |
153 | public synchronized void stop() {
154 | try {
155 | if (scheduler.isStarted() && !scheduler.isShutdown()) {
156 | scheduler.shutdown();
157 | log.info("Stopped Quartz scheduler");
158 | }
159 | } catch (Exception e) {
160 | throw new RuntimeException("Could not stop Quartz scheduler", e);
161 | }
162 | }
163 |
164 | public JobBuilderFactory jobs() {
165 | return jobBuilderFactory;
166 | }
167 |
168 | public StepBuilderFactory steps() {
169 | return stepBuilderFactory;
170 | }
171 |
172 | // ===============
173 | // Private Helpers
174 | // ===============
175 |
176 | private JobDetail jobDetailFor(String jobName) {
177 | JobConfig config = new JobConfig();
178 | config.setName(jobName);
179 | return this.jobDetailFor(config);
180 | }
181 |
182 | private JobDetail jobDetailFor(JobConfig jobConfig) {
183 | JobDetail jobDetail = newJob(QuartzJobLauncher.class)
184 | .withIdentity(jobConfig.getName(), TriggerUtil.QUARTZ_DEFAULT_GROUP)
185 | .usingJobData(QuartzJobLauncher.JOB_NAME, jobConfig.getName()).build();
186 |
187 | if (jobConfig.getProperties() != null) {
188 | jobDetail = new JobParamsDetail(jobDetail, jobConfig.getProperties());
189 | }
190 | return jobDetail;
191 | }
192 | }
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/quartz/QuartzJobLauncher.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz;
2 |
3 | import lombok.extern.slf4j.Slf4j;
4 | import org.quartz.JobDataMap;
5 | import org.quartz.JobDetail;
6 | import org.quartz.JobExecutionContext;
7 | import org.springframework.batch.core.Job;
8 | import org.springframework.batch.core.JobExecution;
9 | import org.springframework.batch.core.JobParameters;
10 | import org.springframework.batch.core.configuration.JobLocator;
11 | import org.springframework.batch.core.launch.JobLauncher;
12 | import org.springframework.scheduling.quartz.QuartzJobBean;
13 |
14 | import com.github.chrisgleissner.springbatchrest.util.JobParamUtil;
15 | import com.github.chrisgleissner.springbatchrest.util.core.JobParamsDetail;
16 |
17 | @Slf4j
18 | public class QuartzJobLauncher extends QuartzJobBean {
19 |
20 | public static final String JOB_NAME = "jobName";
21 | public static final String JOB_LOCATOR = "jobLocator";
22 | public static final String JOB_LAUNCHER = "jobLauncher";
23 |
24 | @Override
25 | protected void executeInternal(JobExecutionContext context) {
26 | String jobName = null;
27 | try {
28 |
29 | JobDetail jobDetail = context.getJobDetail();
30 | JobParameters jobParams = new JobParameters();
31 | if (jobDetail instanceof JobParamsDetail) {
32 | jobParams = JobParamUtil.convertRawToJobParams(((JobParamsDetail) jobDetail).getRawJobParameters());
33 | }
34 |
35 | JobDataMap dataMap = context.getJobDetail().getJobDataMap();
36 | jobName = dataMap.getString(JOB_NAME);
37 |
38 | JobLocator jobLocator = (JobLocator) context.getScheduler().getContext().get(JOB_LOCATOR);
39 | JobLauncher jobLauncher = (JobLauncher) context.getScheduler().getContext().get(JOB_LAUNCHER);
40 |
41 | Job job = jobLocator.getJob(jobName);
42 | log.info("Starting {}", job.getName());
43 | JobExecution jobExecution = jobLauncher.run(job, jobParams);
44 | log.info("{}_{} was completed successfully", job.getName(), jobExecution.getId());
45 | } catch (Exception e) {
46 | log.error("Job {} failed", jobName, e);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/quartz/config/AdHocSchedulerConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz.config;
2 |
3 | import com.github.chrisgleissner.springbatchrest.util.core.config.AdHocBatchConfig;
4 | import org.springframework.context.annotation.ComponentScan;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.context.annotation.Import;
7 |
8 |
9 | @Configuration
10 | @ComponentScan(basePackages = {
11 | "com.github.chrisgleissner.springbatchrest.util.quartz"
12 | })
13 | @Import({AdHocBatchConfig.class, SchedulerConfig.class})
14 | public class AdHocSchedulerConfig {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/util/src/main/java/com/github/chrisgleissner/springbatchrest/util/quartz/config/SchedulerConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz.config;
2 |
3 | import static com.github.chrisgleissner.springbatchrest.util.quartz.QuartzJobLauncher.JOB_LAUNCHER;
4 | import static com.github.chrisgleissner.springbatchrest.util.quartz.QuartzJobLauncher.JOB_LOCATOR;
5 |
6 | import org.quartz.Scheduler;
7 | import org.quartz.SchedulerException;
8 | import org.quartz.impl.StdSchedulerFactory;
9 | import org.springframework.batch.core.configuration.JobLocator;
10 | import org.springframework.batch.core.launch.JobLauncher;
11 | import org.springframework.context.annotation.Bean;
12 | import org.springframework.context.annotation.Configuration;
13 |
14 | @Configuration
15 | public class SchedulerConfig {
16 | @Bean
17 | public Scheduler scheduler(JobLocator jobLocator, JobLauncher jobLauncher) throws SchedulerException {
18 | Scheduler scheduler = new StdSchedulerFactory().getScheduler();
19 | scheduler.getContext().put(JOB_LOCATOR, jobLocator);
20 | scheduler.getContext().put(JOB_LAUNCHER, jobLauncher);
21 | return scheduler;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/DateUtilTest.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util;
2 |
3 | import java.util.Date;
4 | import java.time.Instant;
5 | import java.time.LocalDateTime;
6 | import java.time.OffsetDateTime;
7 |
8 | import org.junit.jupiter.api.Assertions;
9 | import org.junit.jupiter.api.Test;
10 |
11 | public class DateUtilTest {
12 |
13 | @Test
14 | public void testLocalDateConversion() {
15 | Date now = new Date();
16 | LocalDateTime localDateTime = DateUtil.localDateTime(now);
17 | Instant ldtInstant = localDateTime.toInstant(OffsetDateTime.now().getOffset());
18 |
19 | Assertions.assertEquals(ldtInstant, now.toInstant());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/JobParamUtilTest.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util;
2 |
3 | import java.util.Date;
4 |
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.Test;
7 | import org.springframework.batch.core.JobParameter;
8 |
9 | public class JobParamUtilTest {
10 |
11 | @Test
12 | public void testObjectConversionHappy() {
13 | Date date = new Date();
14 | JobParameter dateParam = JobParamUtil.createJobParameter(date);
15 | Assertions.assertEquals(dateParam.getValue(), date);
16 |
17 | Long longVar = new Long(1234);
18 | JobParameter longParam = JobParamUtil.createJobParameter(longVar);
19 | Assertions.assertEquals(longParam.getValue(), longVar);
20 |
21 | Double doubleVar = new Double(123.123);
22 | JobParameter doubleParam = JobParamUtil.createJobParameter(doubleVar);
23 | Assertions.assertEquals(doubleParam.getValue(), doubleVar);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/core/AdHocStarterTest.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core;
2 |
3 | import lombok.extern.slf4j.Slf4j;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.batch.core.Job;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.core.env.Environment;
9 | import org.springframework.test.context.ContextConfiguration;
10 | import org.springframework.test.context.TestPropertySource;
11 | import org.springframework.test.context.junit4.SpringRunner;
12 |
13 | import com.github.chrisgleissner.springbatchrest.util.core.config.AdHocBatchConfig;
14 |
15 | import java.util.HashMap;
16 | import java.util.Optional;
17 | import java.util.Set;
18 | import java.util.concurrent.ConcurrentSkipListSet;
19 | import java.util.concurrent.CountDownLatch;
20 |
21 | import static com.github.chrisgleissner.springbatchrest.util.core.property.JobPropertyResolvers.JobProperties;
22 | import static java.util.concurrent.TimeUnit.SECONDS;
23 | import static org.assertj.core.api.Assertions.assertThat;
24 |
25 | @RunWith(SpringRunner.class)
26 | @ContextConfiguration(classes = AdHocBatchConfig.class)
27 | @TestPropertySource(properties = "foo=bar")
28 | @Slf4j
29 | public class AdHocStarterTest {
30 | private static final String JOB_NAME = "AdHocStarterTest";
31 | private static final int NUMBER_OF_ITERATIONS = 3;
32 | private static final int NUMBER_OF_JOBS_PER_ITERATION = 2;
33 | public static final String PROPERTY_NAME = "foo";
34 |
35 | @Autowired Environment env;
36 | @Autowired private AdHocStarter starter;
37 | @Autowired private JobBuilder jobBuilder;
38 |
39 | @Test
40 | public void startAsynchPropertyResolverConsumerJobWithPropertyMap() throws InterruptedException {
41 | assertPropertyResolution((propertyValue, readPropertyValues, latch)
42 | -> starter.start(createJobFromPropertyResolverConsumer(readPropertyValues, latch), true, propertyMap(propertyValue)));
43 | }
44 |
45 | @Test
46 | public void startAsynchPropertyResolverConsumerJobWithJobConfig() throws InterruptedException {
47 | assertPropertyResolution((propertyValue, readPropertyValues, latch) -> {
48 | createJobFromPropertyResolverConsumer(readPropertyValues, latch);
49 | starter.start(JobConfig.builder()
50 | .name(JOB_NAME)
51 | .property(PROPERTY_NAME, "" + propertyValue)
52 | .asynchronous(true).build());
53 | });
54 | }
55 |
56 | @Test
57 | public void startSynchRunnable() throws InterruptedException {
58 | assertPropertyResolution((propertyValue, readPropertyValues, latch)
59 | -> starter.start(createJobFromRunnable(readPropertyValues, latch), false, propertyMap(propertyValue)));
60 | }
61 |
62 | @Test
63 | public void startAsynchStepExecutionConsumer() throws InterruptedException {
64 | assertPropertyResolution((propertyValue, readPropertyValues, latch)
65 | -> starter.start(createJobFromStepExecutionConsumer(readPropertyValues, latch), true, propertyMap(propertyValue)));
66 | }
67 |
68 | private static HashMap propertyMap(int propertyValue) {
69 | final HashMap propMap = new HashMap<>();
70 | propMap.put(PROPERTY_NAME, propertyValue);
71 | return propMap;
72 | }
73 |
74 | private Job createJobFromRunnable(Set readPropertyValues, CountDownLatch latch) {
75 | return jobBuilder.createJob(JOB_NAME, () -> {
76 | readPropertyValues.add(JobProperties.of(JOB_NAME).getProperty(PROPERTY_NAME));
77 | latch.countDown();
78 | });
79 | }
80 |
81 | private Job createJobFromPropertyResolverConsumer(Set readPropertyValues, CountDownLatch latch) {
82 | return jobBuilder.createJob(JOB_NAME, (propertyResolver) -> {
83 | readPropertyValues.add(propertyResolver.getProperty(PROPERTY_NAME));
84 | latch.countDown();
85 | });
86 | }
87 |
88 | private Job createJobFromStepExecutionConsumer(Set readPropertyValues, CountDownLatch latch) {
89 | return jobBuilder.createJobFromStepExecutionConsumer(JOB_NAME, (stepExecution) -> {
90 | String propertyValue = Optional.ofNullable(stepExecution.getJobParameters().getString(PROPERTY_NAME))
91 | .orElseGet(() -> env.getProperty(PROPERTY_NAME));
92 | readPropertyValues.add(propertyValue);
93 | latch.countDown();
94 | });
95 | }
96 |
97 | private void assertPropertyResolution(JobStarter jobStarter) throws InterruptedException {
98 | // Check that asynchronous execution with property overrides works
99 | final Set readPropertyValues = new ConcurrentSkipListSet<>();
100 | int propertyValue = 0;
101 | for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) {
102 | final CountDownLatch latch = new CountDownLatch(NUMBER_OF_JOBS_PER_ITERATION);
103 | for (int j = 0; j < NUMBER_OF_JOBS_PER_ITERATION; j++) {
104 | jobStarter.startJob(propertyValue++, readPropertyValues, latch);
105 | }
106 | assertThat(latch.await(3, SECONDS)).isTrue();
107 | assertThat(readPropertyValues).hasSize(propertyValue);
108 | }
109 | Thread.sleep(100); // Job completion takes place after latch is counted down
110 | assertThat(JobProperties.of(JOB_NAME).getProperty(PROPERTY_NAME)).isEqualTo("bar");
111 |
112 | // Check that synchronous execution without overrides works
113 | starter.start(JobConfig.builder()
114 | .name(JOB_NAME)
115 | .asynchronous(false)
116 | .build());
117 | assertThat(readPropertyValues).contains("bar");
118 | }
119 |
120 | private interface JobStarter {
121 | void startJob(int propertyValue, Set propertyValues, CountDownLatch latch);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/core/CacheItemWriter.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core;
2 |
3 | import org.springframework.batch.item.ItemWriter;
4 |
5 | import java.util.LinkedList;
6 | import java.util.List;
7 |
8 | import static java.util.Collections.synchronizedList;
9 |
10 | public class CacheItemWriter implements ItemWriter {
11 |
12 | private List items = synchronizedList(new LinkedList<>());
13 |
14 | @Override
15 | public void write(List extends T> items) {
16 | this.items.addAll(items);
17 | }
18 |
19 | public List getItems() {
20 | return items;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/core/JobCompletionNotificationListener.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core;
2 |
3 | import org.springframework.batch.core.BatchStatus;
4 | import org.springframework.batch.core.JobExecution;
5 | import org.springframework.batch.core.listener.JobExecutionListenerSupport;
6 |
7 | import java.util.concurrent.Semaphore;
8 | import java.util.concurrent.TimeUnit;
9 |
10 | public class JobCompletionNotificationListener extends JobExecutionListenerSupport {
11 |
12 | public Semaphore semaphore = new Semaphore(0);
13 |
14 | public void awaitCompletionOfJobs(int numberOfJobs, long maxWaitInMillis) throws InterruptedException {
15 | if (!semaphore.tryAcquire(numberOfJobs, maxWaitInMillis, TimeUnit.MILLISECONDS)) {
16 | throw new RuntimeException("Not all jobs have completed. Not completed: " + semaphore.availablePermits());
17 | }
18 | }
19 |
20 | @Override
21 | public void afterJob(JobExecution jobExecution) {
22 | if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
23 | semaphore.release();
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/core/Person.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.core;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import lombok.NoArgsConstructor;
6 | import lombok.Setter;
7 |
8 | @Data
9 | @Setter
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class Person {
13 | private String firstName;
14 | private String lastName;
15 | }
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/quartz/AdHocSchedulerParamsTest.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz;
2 |
3 | import org.junit.Test;
4 | import org.junit.jupiter.api.AfterAll;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.runner.RunWith;
7 | import org.mockito.ArgumentCaptor;
8 | import org.quartz.Scheduler;
9 | import org.quartz.SchedulerException;
10 | import org.quartz.impl.StdSchedulerFactory;
11 | import org.springframework.batch.core.Job;
12 | import org.springframework.batch.core.JobExecution;
13 | import org.springframework.batch.core.JobParameters;
14 | import org.springframework.batch.core.JobParametersInvalidException;
15 | import org.springframework.batch.core.configuration.JobLocator;
16 | import org.springframework.batch.core.launch.JobLauncher;
17 | import org.springframework.batch.core.launch.support.RunIdIncrementer;
18 | import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
19 | import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
20 | import org.springframework.batch.core.repository.JobRestartException;
21 | import org.springframework.batch.repeat.RepeatStatus;
22 | import org.springframework.beans.factory.annotation.Autowired;
23 | import org.springframework.boot.test.mock.mockito.MockBean;
24 | import org.springframework.context.annotation.Bean;
25 | import org.springframework.context.annotation.Primary;
26 | import org.springframework.test.annotation.DirtiesContext;
27 | import org.springframework.test.annotation.DirtiesContext.MethodMode;
28 | import org.springframework.test.context.ContextConfiguration;
29 | import org.springframework.test.context.junit4.SpringRunner;
30 |
31 | import com.github.chrisgleissner.springbatchrest.util.JobParamUtil;
32 | import com.github.chrisgleissner.springbatchrest.util.core.JobBuilder;
33 | import com.github.chrisgleissner.springbatchrest.util.core.JobConfig;
34 | import com.github.chrisgleissner.springbatchrest.util.quartz.config.AdHocSchedulerConfig;
35 | import com.github.chrisgleissner.springbatchrest.util.quartz.AdHocSchedulerParamsTest.CustomContextConfiguration;
36 |
37 | import static org.mockito.Mockito.timeout;
38 | import static org.mockito.Mockito.verify;
39 | import static org.mockito.Mockito.when;
40 |
41 | import java.time.Instant;
42 | import java.util.Date;
43 | import java.util.HashMap;
44 | import java.util.List;
45 | import java.util.Map;
46 | import java.util.Random;
47 |
48 | import static com.github.chrisgleissner.springbatchrest.util.quartz.QuartzJobLauncher.JOB_LAUNCHER;
49 | import static com.github.chrisgleissner.springbatchrest.util.quartz.QuartzJobLauncher.JOB_LOCATOR;
50 |
51 | /**
52 | * Tests the ad-hoc Quartz scheduling of Spring Batch jobs with parameters
53 | */
54 | @RunWith(SpringRunner.class)
55 | @ContextConfiguration(
56 | classes = {
57 | AdHocSchedulerConfig.class,
58 | CustomContextConfiguration.class
59 | },
60 | name = "mockJobLauncherContext")
61 | public class AdHocSchedulerParamsTest {
62 |
63 | private static final String TRIGGER_EVERY_SECOND = "0/1 * * * * ?";
64 |
65 | @Autowired
66 | private AdHocScheduler scheduler;
67 |
68 | @Autowired
69 | private JobLauncher jobLauncher;
70 |
71 | @Autowired
72 | private JobBuilder jobBuilder;
73 |
74 | @AfterAll
75 | public void afterAll() {
76 | scheduler.stop();
77 | }
78 |
79 | // Override the scheduler's job launcher with a mock so we can verify
80 | // that calls to it contain parameters
81 | protected static class CustomContextConfiguration {
82 |
83 | @MockBean
84 | public JobLauncher mockJobLauncher;
85 |
86 | @Bean
87 | @Primary
88 | public Scheduler scheduler(JobLocator jobLocator) throws SchedulerException {
89 | Scheduler scheduler = new StdSchedulerFactory().getScheduler();
90 | scheduler.getContext().remove(JOB_LOCATOR);
91 | scheduler.getContext().put(JOB_LOCATOR, jobLocator);
92 | scheduler.getContext().remove(JOB_LAUNCHER);
93 | scheduler.getContext().put(JOB_LAUNCHER, mockJobLauncher);
94 | return scheduler;
95 | }
96 | }
97 |
98 | @Test
99 | @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD)
100 | public void paramsAddedToScheduledJobWorks()
101 | throws InterruptedException, JobExecutionAlreadyRunningException, JobRestartException,
102 | JobInstanceAlreadyCompleteException, JobParametersInvalidException, SchedulerException {
103 |
104 | Job job1 = job("j1");
105 | Job job2 = job("j2");
106 |
107 | jobBuilder.registerJob(job1);
108 | jobBuilder.registerJob(job2);
109 |
110 | Map params = new HashMap();
111 | params.put("testParamKey", "testParamValue");
112 |
113 | JobParameters expectedParams = JobParamUtil.convertRawToJobParams(params);
114 |
115 | JobConfig job1Config = JobConfig.builder().name("j1").properties(params).build();
116 | JobConfig job2Config = JobConfig.builder().name("j2").properties(params).build();
117 |
118 | Date now = Date.from(Instant.now().plusMillis(2000));
119 |
120 | scheduler.start();
121 |
122 | when(jobLauncher.run(job1, expectedParams))
123 | .thenReturn(new JobExecution(new Random().nextLong(), expectedParams));
124 |
125 | when(jobLauncher.run(job2, expectedParams))
126 | .thenReturn(new JobExecution(new Random().nextLong(), expectedParams));
127 |
128 | scheduler.schedule(job1Config, now);
129 | scheduler.schedule(job2Config, TRIGGER_EVERY_SECOND);
130 |
131 | ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(Job.class);
132 | ArgumentCaptor jobParamCaptor = ArgumentCaptor.forClass(JobParameters.class);
133 | verify(jobLauncher, timeout(8000).times(2)).run(jobCaptor.capture(), jobParamCaptor.capture());
134 |
135 | List paramsListAfterCall = jobParamCaptor.getAllValues();
136 |
137 | Assertions.assertEquals(paramsListAfterCall.size(), 2);
138 |
139 | for (JobParameters jobParams : paramsListAfterCall) {
140 | Assertions.assertEquals(jobParams.getString("testParamKey"), "testParamValue");
141 | }
142 |
143 | scheduler.pause();
144 | }
145 |
146 | private Job job(String jobName) {
147 | return scheduler.jobs().get(jobName).incrementer(new RunIdIncrementer()) // adds unique parameter on each run so
148 | // that createJob can be rerun
149 | .start(scheduler.steps().get("step").tasklet((contribution, chunkContext) -> {
150 | return RepeatStatus.FINISHED;
151 | }).allowStartIfComplete(true).build()).build();
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/util/src/test/java/com/github/chrisgleissner/springbatchrest/util/quartz/AdHocSchedulerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.chrisgleissner.springbatchrest.util.quartz;
2 |
3 | import org.junit.Test;
4 | import org.junit.jupiter.api.AfterAll;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.runner.RunWith;
8 | import org.springframework.batch.core.Job;
9 | import org.springframework.batch.core.launch.support.RunIdIncrementer;
10 | import org.springframework.batch.repeat.RepeatStatus;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.test.context.ContextConfiguration;
13 | import org.springframework.test.context.junit4.SpringRunner;
14 |
15 | import com.github.chrisgleissner.springbatchrest.util.core.JobBuilder;
16 | import com.github.chrisgleissner.springbatchrest.util.core.JobConfig;
17 | import com.github.chrisgleissner.springbatchrest.util.quartz.config.AdHocSchedulerConfig;
18 |
19 | import java.util.concurrent.CountDownLatch;
20 |
21 | import static java.util.concurrent.TimeUnit.SECONDS;
22 |
23 | import java.time.Instant;
24 | import java.util.Date;
25 |
26 | /**
27 | * Tests the ad-hoc Quartz scheduling of Spring Batch jobs, allowing for
28 | * programmatic scheduling after Spring wiring.
29 | */
30 | @RunWith(SpringRunner.class)
31 | @ContextConfiguration(classes = AdHocSchedulerConfig.class)
32 | public class AdHocSchedulerTest {
33 |
34 | private static final String TRIGGER_EVERY_SECOND = "0/1 * * * * ?";
35 | private static final int NUMBER_OF_EXECUTIONS_PER_JOB = 2;
36 |
37 | @Autowired
38 | private AdHocScheduler scheduler;
39 |
40 | @Autowired
41 | private JobBuilder jobBuilder;
42 |
43 | private CountDownLatch latch1 = new CountDownLatch(NUMBER_OF_EXECUTIONS_PER_JOB);
44 | private CountDownLatch latch2 = new CountDownLatch(NUMBER_OF_EXECUTIONS_PER_JOB);
45 |
46 | @BeforeEach
47 | public void before() {
48 | latch1 = new CountDownLatch(NUMBER_OF_EXECUTIONS_PER_JOB);
49 | latch2 = new CountDownLatch(NUMBER_OF_EXECUTIONS_PER_JOB);
50 | }
51 |
52 | @AfterAll
53 | public void afterAll() {
54 | scheduler.stop();
55 | }
56 |
57 | @Test
58 | public void scheduleCronWithJobReferenceWorks() throws InterruptedException {
59 | scheduler.schedule("j1", job("j1", latch1), TRIGGER_EVERY_SECOND);
60 | scheduler.schedule("j2", job("j2", latch2), TRIGGER_EVERY_SECOND);
61 | scheduler.start();
62 |
63 | latch1.await(4, SECONDS);
64 | latch2.await(4, SECONDS);
65 | scheduler.pause();
66 | }
67 |
68 | @Test
69 | public void scheduleWithJobConfigAndDateWorks() throws InterruptedException {
70 | Job job1 = job("j1", latch1);
71 | Job job2 = job("j2", latch2);
72 |
73 | jobBuilder.registerJob(job1);
74 | jobBuilder.registerJob(job2);
75 |
76 | JobConfig job1Config = JobConfig.builder().name("j1").build();
77 | JobConfig job2Config = JobConfig.builder().name("j2").build();
78 |
79 | Date oneSecondFromNow = Date.from(Instant.now().plusMillis(1000));
80 |
81 | scheduler.schedule(job1Config, oneSecondFromNow);
82 | scheduler.schedule(job2Config, oneSecondFromNow);
83 | scheduler.start();
84 |
85 | latch1.await(4, SECONDS);
86 | latch2.await(4, SECONDS);
87 | scheduler.pause();
88 | }
89 |
90 | @Test
91 | public void scheduleWithJobConfigAndCronWorks() throws InterruptedException {
92 | Job job1 = job("j1", latch1);
93 | Job job2 = job("j2", latch2);
94 |
95 | jobBuilder.registerJob(job1);
96 | jobBuilder.registerJob(job2);
97 |
98 | JobConfig job2Config = JobConfig.builder().name("j2").build();
99 |
100 | scheduler.schedule("j1", job1, TRIGGER_EVERY_SECOND);
101 | scheduler.schedule(job2Config, TRIGGER_EVERY_SECOND);
102 | scheduler.start();
103 |
104 | latch1.await(4, SECONDS);
105 | latch2.await(4, SECONDS);
106 | scheduler.pause();
107 | }
108 |
109 | @Test
110 | public void happyCaseSchedulerStartPauseResumeNoThrow() {
111 | Assertions.assertDoesNotThrow(() -> {
112 | scheduler.start();
113 | });
114 | Assertions.assertDoesNotThrow(() -> {
115 | scheduler.pause();
116 | });
117 | Assertions.assertDoesNotThrow(() -> {
118 | scheduler.resume();
119 | });
120 | // Future - handle & test the case where the scheduler has been shutdown and
121 | // needs re-initialization
122 | // https://stackoverflow.com/questions/15020625/quartz-how-to-shutdown-and-restart-the-scheduler
123 | }
124 |
125 | @Test
126 | public void exceptionForBadJobConfigDate() {
127 | Assertions.assertThrows(RuntimeException.class, () -> {
128 | scheduler.schedule(JobConfig.builder().name(null).asynchronous(false).build(), new Date());
129 | });
130 | }
131 |
132 | @Test
133 | public void exceptionForBadJobConfigCron() {
134 | Assertions.assertThrows(RuntimeException.class, () -> {
135 | scheduler.schedule(JobConfig.builder().name(null).asynchronous(false).build(), TRIGGER_EVERY_SECOND);
136 | });
137 | }
138 |
139 | @Test
140 | public void exceptionForBadParamsCron() {
141 | Assertions.assertThrows(RuntimeException.class, () -> {
142 | scheduler.schedule(null, null, TRIGGER_EVERY_SECOND);
143 | });
144 | }
145 |
146 | private Job job(String jobName, CountDownLatch latch) {
147 | return scheduler.jobs().get(jobName).incrementer(new RunIdIncrementer()) // adds unique parameter on each run so
148 | // that createJob can be rerun
149 | .start(scheduler.steps().get("step").tasklet((contribution, chunkContext) -> {
150 | latch.countDown();
151 | return RepeatStatus.FINISHED;
152 | }).allowStartIfComplete(true).build()).build();
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/util/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.batch.job.enabled=false
2 | server.port=9090
--------------------------------------------------------------------------------
/util/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------