it-swarm.com.ru

Как запустить запланированное весеннее пакетное задание?

Я хочу иметь возможность начать свою работу с контроллера REST, затем, когда задание запускается, оно должно выполняться по расписанию, пока я не остановлю его снова с помощью REST.

Итак, это мой контроллер:

@RestController
public class LauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

    @RequestMapping("/launch")
    public String launch() throws Exception {
             ...
            jobLauncher.run(job, jobParameters);
    }

Это некоторая часть Batch Conf:

@Configuration
@EnableBatchProcessing
@EnableScheduling
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Scheduled(cron = "0/5 * * * * ?")
    @Bean
    public Job job() {
        return jobBuilderFactory.get("job")
                .incrementer(new RunIdIncrementer())
                .flow(step1())
                .end()
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .build();
    }

Я также установил свойство spring.batch.job.enabled = false, поскольку я не хочу, чтобы задания запускались сразу после запуска Spring Boot App.

Теперь я могу позвонить в мой Rest api lauch, и работа выполняется, но только один раз. Планировщик не работает. И я не мог понять это, где именно я должен определить мою @Scheduled Annotation .. 

8
akcasoy

Я хотел бы подойти к этому так, что запланированное задание выполняется всегда, но оно делает что-то, только когда флаг установлен в true:

@Component
class ScheduledJob {

    private final AtomicBoolean enabled = new AtomicBoolean(false);

    @Scheduled(fixedRate = 1000)
    void execute() {
        if (enabled.get()) {
            // run spring batch here.
        }
    }

    void toggle() {
        enabled.set(!enabled.get());
    }

}

и контроллер:

@RestController
class HelloController {

    private final ScheduledJob scheduledJob;

    // constructor

    @GetMapping("/launch")
    void toggle() {
        scheduledJob.toggle();
    }

}
11
Maciej Walkowiak

Во первых вы определяете работу:

@Bean
@Qualifier("fancyScheduledJob")
public Job job() {
    return jobBuilderFactory.get("job")
            .incrementer(new RunIdIncrementer())
            .flow(step1())
            .end()
            .build();
}

Во-вторых, вы инициируете выполнение этой работы: 

@Autowired
@Qualifier(value = "fancyScheduledJob")
private Job job;

@Autowired
private JobLauncher jobLauncher;

@Scheduled(cron = "0/5 * * * * ?")
public void launch() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobInstanceAlreadyExistsException, NoSuchJobException {

    jobLauncher.run(job, JobParametersBuilder()
            .addLong("launchTime", System.currentTimeMillis())
            .toJobParameters())
}

Также обратите внимание, что введен параметр «launchTime»: по умолчанию пакетная пружина запрещает запуск задания с такими же значениями параметров. 

В то время как ваш график довольно плотный - каждые 5 секунд вы должны знать о параллелизме. Или, если вы хотите быть уверены, что в каждый момент времени выполняется только 1 экземпляр задания, вы можете настроить собственный однопоточный модуль запуска заданий: 

@Bean(name = "fancyJobExecutorPool")
public TaskExecutor singleThreadedJobExecutorPool() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(1);
    executor.setQueueCapacity(100500);
    executor.setThreadNamePrefix("fancy-job-batch-");
    return executor;
}

@Bean(name = "fancyJobLauncher")
public JobLauncher singleThreadedJobLauncher(JobRepository jobRepository)
{
    SimpleJobLauncher sjl = new SimpleJobLauncher();
    sjl.setJobRepository(jobRepository);
    sjl.setTaskExecutor(singleThreadedJobExecutorPool());
    return sjl;
}

И используйте этот однопоточный модуль запуска заданий во время запуска.

@Autowired
@Qualifier("fancyJobLauncher")
private JobLauncher jobLauncher;

При этом ваши экземпляры заданий будут выполняться один за другим (но это не ограничивает параллельное выполнение шагов внутри вашей работы).

5
Ilya Dyoshin

В этом решении вы сможете планировать и отменять расписание заданных заданий, используя http-запросы. В этом примере мы создадим ежедневное, еженедельное и одноразовое задание. Приложение использует Quartz.

<!--Quartz Scheduler -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.3</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

Сначала мы должны создать класс AutowiringSpringBeanJobFactory extends SpringBeanJobFactory

  • Подкласс {@link AdaptableJobFactory}, который также поддерживает Spring-style * внедрение зависимости от свойств бина. Это по сути, прямой * эквивалент Spring {@link QuartzJobBean} в форме кварца * {@link org.quartz.spi.JobFactory}. * *

    Применяет контекст планировщика, карту данных задания и карту данных триггера записи * как значения свойств бина. Если нет соответствующего свойства bean-компонента найдено, запись * по умолчанию просто игнорируется. Это аналогично Поведение QuartzJobBean.

public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    private transient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        beanFactory = applicationContext.getAutowireCapableBeanFactory();        
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

Вторая часть заключается в настройке конфигурации кварца. В этом конфиге нам нужно создать 

  • SchedulerFactoryBean где мы устанавливаем глобальную конфигурацию и контекст приложения,
  • JobDetailFactoryBean где мы устанавливаем нашу работу, jobGroup и класс,

  • CronTriggerFactoryBean где мы устанавливаем выражение cron.

QuartzConfig.class

@Configuration
public class QuartzConfig {

    @Autowired
    ApplicationContext context;

    @Bean
    public SchedulerFactoryBean quartzScheduler(){
        SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();
        quartzScheduler.setOverwriteExistingJobs(true);
        quartzScheduler.setSchedulerName("job-scheduler");
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(context);
        quartzScheduler.setJobFactory(jobFactory);
        return quartzScheduler;
    }

    @Bean
    @Scope(value = "prototype")
    public JobDetailFactoryBean getJobBean(String jobName, String jobGroup, Class<?> clazz){
        JobDetailFactoryBean bean = new JobDetailFactoryBean();
        bean.setJobClass(clazz);
        bean.setGroup(jobGroup);
        bean.setName(jobName);
        return bean;
    }

    @Bean
    @Scope(value = "prototype")
    public CronTriggerFactoryBean getCronTriggerBean(String cronExpression, String triggerGroup){
        CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
        bean.setCronExpression(cronExpression);
        bean.setGroup(triggerGroup);
        return bean;
    }
}

Итак, после того, как конфигурация завершена, мы можем создавать рабочие места, в которых будет размещаться бизнес-логика. Для этого мы должны создать класс, который реализует Job

@Component
public class DailyJob implements Job{

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Daily Job runs!");
    }
}

Класс DailyJob теперь готов к расписанию. Мы хотим запланировать эту работу извне через http-запрос. В этом примере у нас есть контроллер, куда мы можем отправить имя задания и выражение cron для планирования dailyJob.

@Controller
public class JobController {

    @Autowired
    private Scheduler scheduler;
    @Autowired
    private ApplicationContext context;;

    @ResponseBody
    @RequestMapping(value = "/job/create/daily", method = RequestMethod.POST)
    public ResponseEntity<JobModel> dailyJob(@RequestBody JobModel jobModel) throws SchedulerException {
        JobDetail jobDetail = context.getBean(
                JobDetail.class, jobModel.getName(), "MyDailyJob", DailyJob.class);
        Trigger cronTrigger = context.getBean(
                Trigger.class, jobModel.getCronExpression(), "MyDailyJob");

        scheduler.scheduleJob(jobDetail, cronTrigger);

        return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);
    }
}

Здесь мы видим, что мы отправим почтовый запрос с JobModel как @RequestBody. JobModel - это простое Pojo с двумя атрибутами name и cronExpression оба типа Strings.

В этом методе мы должны создать экземпляры bean-компонентов, которые мы настроили ранее в нашем классе конфигурации. Сначала создайте JobDetail с Quartz JobDetail.class, названием вашей работы, названием группы и класса, который должен быть запланирован (в данном случае DailyJob.class). После этого мы должны создать Trigger with Quartz Trigger.class, cronExpression и имя группы.

После того, как оба бина созданы, нам нужно запланировать работу сейчас. Итак, у нас есть автоматическая разводка Quartz Scheduler для планирования работы. После этого работа включена и готова выполнять свою работу.

Итак, давайте проверим материал. Запустите приложение и отправьте почтовый запрос на /job/create/daily:

{"name":"Job 1", "cronExpression":"0 * * * * ?"}

Здесь мы говорим, что работа должна выполняться каждую минуту (просто чтобы убедиться, что все работает). В вашей консоли вы должны видеть каждую минуту Daily Job runs!.

И вот некоторые дополнительные вещи, которые вы можете сделать. Например, получить список запланированных работ:

 @ResponseBody
 @RequestMapping("job/list")
 public List<String> jobList() throws SchedulerException {
     return scheduler.getJobGroupNames();
 }

Чтобы удалить работу, вы также можете создать конечные точки. Например: 

@ResponseBody
@RequestMapping(value = "job/delete/daily", method = RequestMethod.POST)
public ResponseEntity<Boolean> deleteJob(@RequestBody JobModel jobModel) throws SchedulerException {
    JobKey jobKey = new JobKey(jobModel.getName(), "MyDailyJob");
    return new ResponseEntity<Boolean>(scheduler.deleteJob(jobKey), HttpStatus.OK);
}

Вы можете создавать различные конечные точки, чтобы получать информацию о выполняемых в данный момент заданиях, частоте их выполнения, перепланировании заданий и т.д. Важно только то, что ваше имя и рабочая группа (в нашем случае "MyDailyJob") могут использоваться повторно. Эта информация необходима для создания jobKey.

П.С .: Просто чтобы показать другие сопоставления для других работ:

@ResponseBody
@RequestMapping(value = "/job/create/weekly", method = RequestMethod.POST)
public ResponseEntity<JobModel> weeklyJob(@RequestBody JobModel jobModel) throws SchedulerException {
    JobDetail jobDetail = context.getBean(JobDetail.class, jobModel.getName(), JobGroup.WEEKLY_GROUP.name(),
            WeeklyJob.class);
    Trigger cronTrigger = context.getBean(Trigger.class, jobModel.getCronExpression(),
            JobGroup.WEEKLY_GROUP.name());

    scheduler.scheduleJob(jobDetail, cronTrigger);

    return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);

}

@ResponseBody
@RequestMapping(value = "/job/create/oneTime", method = RequestMethod.POST)
public ResponseEntity<JobModel> oneTimeJob(@RequestBody JobModel jobModel) throws SchedulerException {
    JobDetail jobDetail = context.getBean(JobDetail.class, jobModel.getName(), JobGroup.ONE_TIME_GROUP.name(),
            OneTimeJob.class);
    Trigger cronTrigger = context.getBean(Trigger.class, jobModel.getCronExpression(),
            JobGroup.ONE_TIME_GROUP.name());

    scheduler.scheduleJob(jobDetail, cronTrigger);

    return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);
}

Полная заявка на github

3
Patrick

@Scheduled определяется для метода, а не для компонента. Так что создайте новый класс, который будет бобом

public class BatchConfiguration {
...
@Bean
public Job job() {
    return new Job();
}

новый класс:

public class Job {

@Scheduled(cron = "0/5 * * * * ?")
public Job job() {
    return jobBuilderFactory.get("job")
            .incrementer(new RunIdIncrementer())
            .flow(step1())
            .end()
            .build();
}
1
user7294900