web-dev-qa-db-fra.com

Comment déclencher un travail Spring Batch planifié?

Je veux pouvoir démarrer mon travail avec un contrôleur REST. Lorsque le travail est démarré, il doit être exécuté de manière planifiée, jusqu'à ce que je l'arrête à nouveau avec REST.

Donc voici mon contrôleur:

@RestController
public class LauncherController {

    @Autowired
    JobLauncher jobLauncher;

    @Autowired
    Job job;

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

Ceci est une partie de la conf de lot:

@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();
    }

J'ai également défini la propriété spring.batch.job.enabled = false, car je ne souhaite pas que les travaux soient exécutés dès le démarrage de Spring Boot App.

Maintenant, je peux appeler mon poste de repos Api, et le travail est exécuté, mais une seule fois. Le planificateur ne fonctionne pas. Et je ne pouvais pas comprendre exactement où je devrais définir mon @Scheduled Annotation .. 

8
akcasoy

Je l’approcherais d’une certaine manière, le travail planifié s’exécutant toujours, mais il ne fait quelque chose que lorsque le drapeau est défini sur 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());
    }

}

et un contrôleur:

@RestController
class HelloController {

    private final ScheduledJob scheduledJob;

    // constructor

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

}
11
Maciej Walkowiak

En premier lieu, vous définissez le travail:

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

En second lieu, vous lancez l'exécution de ce travail: 

@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())
}

Notez également que le paramètre "launchTime" est introduit: par défaut, Spring batch empêche le lancement du travail avec les mêmes valeurs de paramètre. 

Bien que votre emploi du temps soit assez serré - toutes les 5 secondes, vous devez être conscient de la concurrence. Ou, si vous voulez être assuré qu'à chaque moment, une seule instance du travail est exécutée, vous pouvez configurer le lanceur de travaux à un seul thread personnalisé: 

@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;
}

Et utilisez ce lanceur de tâches à thread unique au moment du lancement.

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

Avec cela, vos instances de travail seront exécutées une par une (mais cela ne limite pas l'exécution parallèle des étapes à l'intérieur de votre travail).

5
Ilya Dyoshin

Dans cette solution, vous pourrez planifier et annuler la planification de travaux prédéfinis à l'aide de requêtes http. Dans cet exemple, nous allons créer un travail quotidien, hebdomadaire et unique. L'application utilise 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>

Nous devons d’abord créer une classe AutowiringSpringBeanJobFactory etend SpringBeanJobFactory

  • Sous-classe de {@link AdaptableJobFactory} qui prend également en charge Injection de dépendance printanière * sur les propriétés du haricot. C'est essentiellement l'équivalent direct * de {@link QuartzJobBean} .__ de Spring. sous la forme d'un Quartz * {@link org.quartz.spi.JobFactory}. * *

    Applique le contexte du planificateur, le mappage de données de travail et le mappage de données de déclenchement entrées * en tant que valeurs de propriété de haricot. Si aucune propriété de haricot correspondante est trouvé, l’entrée * est simplement ignorée par défaut. Ceci est analogue à Le comportement de 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;
    }
}

La deuxième partie consiste à configurer la configuration du quartz. Dans cette configuration, nous devons créer un 

  • SchedulerFactoryBean où nous définissons la configuration globale et le contexte de l'application,
  • JobDetailFactoryBean où nous avons défini notre travail, le groupe de travail et la classe,

  • CronTriggerFactoryBean où nous définissons l'expression de 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;
    }
}

Ainsi, une fois la configuration terminée, nous sommes maintenant en mesure de créer nos emplois là où la logique métier sera placée. Pour cela, nous devons créer une classe qui implémente Job

@Component
public class DailyJob implements Job{

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

La classe DailyJob est maintenant prête à être planifiée. Nous voulons planifier ce travail de l'extérieur via une requête http. Dans cet exemple, nous avons un contrôleur où nous pouvons envoyer le nom du travail et l'expression cron pour planifier la variable 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);
    }
}

Nous voyons ici que nous enverrons une demande de publication avec une variable JobModel sous la forme @RequestBody. JobModel est un simple Pojo avec deux attributs name et cronExpression les deux chaînes.

Dans cette méthode, nous devons créer les instances de bean que nous avons configurées précédemment dans notre classe de configuration. Créez d'abord JobDetail avec Quartz JobDetail.class, le nom de votre travail, le nom du groupe et la classe à planifier (dans ce cas, DailyJob.class). Ensuite, nous devons créer le déclencheur avec Quartz Trigger.class, l’expression cronExpression et le nom du groupe.

Une fois les deux beans créés, nous devons planifier le travail maintenant. Nous avons donc câblé Quartz Scheduler pour programmer le travail. Ensuite, le travail est activé et prêt à être exécuté.

Alors testons les choses. Démarrez l'application et envoyez une demande de publication à /job/create/daily:

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

Ici, nous disons que le travail doit être exécuté toutes les minutes (juste pour voir que tout fonctionne). Dans votre console, vous devriez voir chaque minute Daily Job runs!.

Et voici quelques choses supplémentaires que vous pouvez faire. Par exemple, obtenez une liste des travaux planifiés:

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

Pour supprimer un travail, vous pouvez également créer des noeuds finaux. Par exemple: 

@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);
}

Vous êtes libre de créer de nombreux systèmes d'extrémité différents pour obtenir des informations sur les travaux en cours, leur fréquence d'exécution, les replanifier, etc. Il est important que votre nom de travail et le groupe de travail (dans notre cas "MyDailyJob") soient réutilisables. Ces informations sont nécessaires pour créer le jobKey.

P.S .: Juste pour montrer les autres mappages pour les autres jobs:

@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);
}

L'application complète est sur github

3
Patrick

@Scheduled est défini sur une méthode et non sur un bean. Alors créez une nouvelle classe qui sera un haricot

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

nouvelle classe:

public class Job {

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