Статьи

Выполнить весеннее пакетное задание с кварцем

Эй, ребята. В этом руководстве мы увидим, как выполняется задание Spring Batch с использованием планировщика Quartz. Если вы не уверены в основах Spring Batch, вы можете посетить мой учебник здесь

Теперь, как мы знаем, задания Spring Batch используются всякий раз, когда мы хотим запустить какой-либо специфичный для бизнеса код или запустить / сгенерировать любые отчеты в любое конкретное время / день. Есть два способа реализации рабочих мест:  tasklet  и  chunks . В этом уроке я создам простую работу, используя a  tasklet, которая напечатает   logger. Основная идея здесь заключается в том, какие конфигурации необходимы для запуска этого задания. Мы будем использовать Spring Boot для начальной загрузки нашего приложения.

В pom.xml нам требуется две зависимости для наличия Spring Batch и Quartz в нашем приложении.

<!-- https://mvnrepository.com/artifact/org.springframework.batch/spring-batch-core -->
<dependency>
    <groupId>org.springframework.batch</groupId>
    <artifactId>spring-batch-core</artifactId>
    <version>4.0.1.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>

Теперь давайте посмотрим, какие конфигурации нам нужны в нашем коде для запуска задания.

1. BatchConfiguration.java:

package com.category.batch.configurations;



import org.springframework.batch.core.configuration.JobRegistry;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;

import org.springframework.batch.core.configuration.support.ApplicationContextFactory;

import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar;

import org.springframework.batch.core.configuration.support.DefaultJobLoader;

import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;

import org.springframework.batch.core.configuration.support.MapJobRegistry;

import org.springframework.batch.core.launch.JobLauncher;

import org.springframework.batch.core.launch.support.SimpleJobLauncher;

import org.springframework.batch.core.repository.JobRepository;

import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean;

import org.springframework.batch.support.transaction.ResourcelessTransactionManager;

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;



@Configuration

@EnableBatchProcessing

@Import({
    BatchJobsDetailedConfiguration.class
})

public class BatchConfiguration {



    @Bean

    public JobRegistry jobRegistry() {

        return new MapJobRegistry();

    }





    @Bean

    public ResourcelessTransactionManager transactionManager() {

        return new ResourcelessTransactionManager();

    }



    @Bean

    public JobRepository jobRepository(ResourcelessTransactionManager transactionManager) throws Exception {

        MapJobRepositoryFactoryBean mapJobRepositoryFactoryBean = new MapJobRepositoryFactoryBean(transactionManager);

        mapJobRepositoryFactoryBean.setTransactionManager(transactionManager);

        return mapJobRepositoryFactoryBean.getObject();

    }



    @Bean

    public JobLauncher jobLauncher(JobRepository jobRepository) throws Exception {

        SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher();

        simpleJobLauncher.setJobRepository(jobRepository);

        simpleJobLauncher.afterPropertiesSet();

        return simpleJobLauncher;

    }



    @Bean

    public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {

        JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();

        jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry());

        return jobRegistryBeanPostProcessor;

    }





}

 Пойдем один за другим:

  •  @ConfigurationЭто указывает, что этот класс будет содержать bean-компоненты и будет создан во время загрузки.
  •  @EnableBatchProcessing: Это включает функции Spring Batch и предоставляет базовую конфигурацию для настройки пакетных заданий.
  •  @Import({BatchJobsDetailedConfiguration.class}): Это импортирует некоторые другие необходимые конфигурации, которые мы увидим позже.
  •  JobRegistry: Этот интерфейс используется для регистрации заданий.
  •  ResourcelessTransactionManager: Этот класс используется, когда вы хотите запустить задание, используя любое постоянство базы данных.
  •  JobRepository: Содержит все метаданные задания, которые возвращают   MapJobRepositoryFactoryBean использованные для непостоянных реализаций DAO.
  •  JobLauncher: Используется для запуска задания, требует jobRepository в качестве зависимости.
  •  JobRegistryBeanPostProcessor: Используется для регистрации задания в  jobRegistry, которое возвращает   jobRegistry.

Теперь перейдем к импортированному классу.

2. BatchJobsDetailedConfiguration.java:

package com.category.batch.configurations;



import java.util.HashMap;

import java.util.Map;



import org.springframework.batch.core.configuration.JobRegistry;

import org.springframework.batch.core.configuration.support.ApplicationContextFactory;

import org.springframework.batch.core.configuration.support.GenericApplicationContextFactory;

import org.springframework.batch.core.launch.JobLauncher;

import org.springframework.batch.core.launch.NoSuchJobException;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.quartz.CronTriggerFactoryBean;

import org.springframework.scheduling.quartz.JobDetailFactoryBean;

import org.springframework.scheduling.quartz.SchedulerFactoryBean;



import com.category.batch.job.JobLauncherDetails;

import com.category.batch.reports.config.ReportsConfig;



@Configuration

public class BatchJobsDetailedConfiguration {



    @Autowired

    private JobLauncher jobLauncher;



    @Bean(name = "reportsDetailContext")

    public ApplicationContextFactory getApplicationContext() {

        return new GenericApplicationContextFactory(ReportsConfig.class);

    }



    @Bean(name = "reportsDetailJob")

    public JobDetailFactoryBean jobDetailFactoryBean() {

        JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();

        jobDetailFactoryBean.setJobClass(JobLauncherDetails.class);

        jobDetailFactoryBean.setDurability(true);

        Map < String, Object > map = new HashMap < > ();

        map.put("jobLauncher", jobLauncher);

        map.put("jobName", ReportsConfig.jobName);

        jobDetailFactoryBean.setJobDataAsMap(map);



        return jobDetailFactoryBean;



    }



    @Bean(name = "reportsCronJob")

    public CronTriggerFactoryBean cronTriggerFactoryBean() {

        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();

        cronTriggerFactoryBean.setJobDetail(jobDetailFactoryBean().getObject());

        cronTriggerFactoryBean.setCronExpression("0 0/1 * 1/1 * ? *");



        return cronTriggerFactoryBean;

    }



    @Bean

    public SchedulerFactoryBean schedulerFactoryBean(JobRegistry jobRegistry) throws NoSuchJobException {

        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

        schedulerFactoryBean.setTriggers(cronTriggerFactoryBean().getObject());

        schedulerFactoryBean.setAutoStartup(true);

        Map < String, Object > map = new HashMap < > ();

        map.put("jobLauncher", jobLauncher);

        map.put("jobLocator", jobRegistry);

        schedulerFactoryBean.setSchedulerContextAsMap(map);

        return schedulerFactoryBean;


    }

}

Давайте углубимся в это:

  •  ApplicationContextFactory: Этот интерфейс в первую очередь полезен при создании нового при  ApplicationContext выполнении задания. Лучше создать отдельную  applicationContext работу для каждой работы.

  •  JobDetailFactoryBean: Используется для создания экземпляра детализации задания Quartz. Этот класс установит класс работы, который мы увидим позже. Он создает карту, которая будет определять и устанавливать имя задания с использованием класса и  joblauncher.

  •  CronTriggerFactoryBean: Используется для создания cron экземпляра триггера Quartz  . Это установит  jobDetail созданное ранее, а затем  cron выражение, когда будет выполняться это задание. Вы можете установить  cron выражения в соответствии с вашими потребностями. Выражения Cron могут быть рассчитаны с http://cronmaker.com.

  •  SchedulerFactoryBean: Используется для создания экземпляра планировщика Quartz и позволяет для регистрации  JobDetails ,  Calendars и Triggers, автоматический запуск планировщика на инициализацию и закрытие его на уничтожение.

Давайте проверим  JobLauncherDetails класс:

3. JobLauncherDetails.java:

package com.category.batch.job;







import java.util.Map;



import org.quartz.JobDataMap;

import org.quartz.JobExecutionContext;

import org.quartz.JobExecutionException;

import org.springframework.batch.core.Job;

import org.springframework.batch.core.JobParameters;

import org.springframework.batch.core.JobParametersBuilder;

import org.springframework.batch.core.JobParametersInvalidException;

import org.springframework.batch.core.configuration.JobLocator;

import org.springframework.batch.core.configuration.JobRegistry;

import org.springframework.batch.core.launch.JobLauncher;

import org.springframework.batch.core.launch.NoSuchJobException;

import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;

import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;

import org.springframework.batch.core.repository.JobRestartException;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.quartz.QuartzJobBean;





public class JobLauncherDetails extends QuartzJobBean {







    static final String JOB_NAME = "jobName";



    public void setJobLocator(JobLocator jobLocator) {

        this.jobLocator = jobLocator;

    }



    public void setJobLauncher(JobLauncher jobLauncher) {

        this.jobLauncher = jobLauncher;

    }



    private JobLocator jobLocator;



    private JobLauncher jobLauncher;



    @Override

    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {

        JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis()).toJobParameters();

        try {

            Map < String, Object > jobDataMap = jobExecutionContext.getMergedJobDataMap();

            String jobName = (String) jobDataMap.get(JOB_NAME);

            jobLauncher.run(jobLocator.getJob(jobName), jobParameters);

        } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException

            |
            JobParametersInvalidException e) {

            e.printStackTrace();

        } catch (NoSuchJobException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        }

    }


}

Этот класс имеет переопределенный  executeInternal метод  QuartzJobBean класса, который берет  jobdetails из карты, которые уже были установлены перед некоторыми из  jobParameters , и затем выполняет,  jobLauncher.run() чтобы выполнить задание, как видно из кода.

Давайте посетим  ReportsConfig класс.

4.ReportsConfig.java:

package com.category.batch.reports.config;



import org.springframework.batch.core.Job;

import org.springframework.batch.core.Step;

import org.springframework.batch.core.configuration.DuplicateJobException;

import org.springframework.batch.core.configuration.JobRegistry;

import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;

import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;

import org.springframework.batch.core.configuration.support.ReferenceJobFactory;

import org.springframework.batch.core.step.tasklet.Tasklet;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;



import com.category.batch.reports.tasklet.ReportTasklet;

@Configuration

public class ReportsConfig {





    @Autowired

    private JobRegistry jobRegistry;



    public final static String jobName = "ReportsJob1";

    public JobBuilderFactory getJobBuilderFactory() {

        return jobBuilderFactory;

    }



    public void setJobBuilderFactory(JobBuilderFactory jobBuilderFactory) {

        this.jobBuilderFactory = jobBuilderFactory;

    }



    @Autowired

    private Tasklet taskletstep;

    @Autowired

    private JobBuilderFactory jobBuilderFactory;



    @Autowired

    private StepBuilderFactory stepBuilderFactory;



    @Bean

    public ReportTasklet reportTasklet() {

        return new ReportTasklet();



    }



    @Bean

    public Job job() throws DuplicateJobException {

        Job job = getJobBuilderFactory().get(jobName).start(getStep()).build();

        return job;

    }



    @Bean

    public Step getStep() {



        return stepBuilderFactory.get("step").tasklet(reportTasklet()).build();

    }



}

Основное назначение класса — иметь конфигурации, связанные с каждой работой. У вас будет отдельный конфиг для каждой работы, как это. Как видите, мы создаем  tasklet здесь, что мы увидим позже. Кроме того, мы определяем и возвращаем  Job, шаг, используя  JobBuilderFactory,   и  StepBuilderFactory. Эти фабрики автоматически установят  JobRepository для вас.

Давайте перейдем к тому ReportTasklet, что наша работа должна быть запущена.

5. ReportTasklet.java:

package com.category.batch.reports.tasklet;



import java.util.logging.Logger;



import org.springframework.batch.core.StepContribution;

import org.springframework.batch.core.scope.context.ChunkContext;

import org.springframework.batch.core.step.tasklet.Tasklet;

import org.springframework.batch.repeat.RepeatStatus;

import org.springframework.context.annotation.Configuration;



@Configuration

public class ReportTasklet implements Tasklet {



    private static final Logger logger = Logger.getLogger(ReportTasklet.class.getName());

    @Override

    public RepeatStatus execute(StepContribution arg0, ChunkContext arg1) {



        try {

            logger.info("Report's Job is running. Add your business logic here.........");

        } catch (Exception e) {

            e.printStackTrace();

        }

        return RepeatStatus.FINISHED;

    }

}

Этот класс имеет метод execute, который будет запускаться при выполнении задания  jobLauncher.run() из  JobLauncherDetails класса. Вы можете определить свою бизнес-логику, которая должна быть выполнена здесь.

Нам понадобится некоторая конфигурация в application.properties, как показано ниже:

6. application.properties

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring.batch.job.enabled=false

Первое свойство требуется для отключения источника данных — только для целей тестирования и не требуется в рабочей среде.

Второе свойство — когда перед запуском сервера выполняется задание. Чтобы избежать этого, нам нужно это свойство.

Теперь, наконец, перейдем к классу приложения. Это должно быть само за себя.

7. BatchApplication.java:

package com.category.batch;



import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;



@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})

public class BatchApplication {



public static void main(String[] args) {

SpringApplication.run(BatchApplication.class, args);

}

}

Достаточно конфигураций! Давайте запустим это приложение и посмотрим вывод. Мы установили на  cron 1 минуту. Через 1 минуту задание будет запущено.

2018-09-14 11:04:18.648 INFO 7008 --- [ost-startStop-1] org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'schedulerFactoryBean' initialized from an externally provided properties instance.

2018-09-14 11:04:18.648 INFO 7008 --- [ost-startStop-1] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.0

2018-09-14 11:04:18.653 INFO 7008 --- [ost-startStop-1] org.quartz.core.QuartzScheduler : JobFactory set to: org.springframework.scheduling.quartz.AdaptableJobFactory@b29e7d6

2018-09-14 11:04:19.578 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]

2018-09-14 11:04:20.986 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@74a6e16d: startup date [Fri Sep 14 11:04:12 IST 2018]; root of context hierarchy

2018-09-14 11:04:21.264 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)

2018-09-14 11:04:21.268 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)

2018-09-14 11:04:21.356 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]

2018-09-14 11:04:21.356 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]

2018-09-14 11:04:22.526 INFO 7008 --- [ost-startStop-1] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup

2018-09-14 11:04:22.555 INFO 7008 --- [ost-startStop-1] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 2147483647

2018-09-14 11:04:22.556 INFO 7008 --- [ost-startStop-1] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now

2018-09-14 11:04:22.556 INFO 7008 --- [ost-startStop-1] org.quartz.core.QuartzScheduler : Scheduler schedulerFactoryBean_$_NON_CLUSTERED started.

2018-09-14 11:04:22.578 INFO 7008 --- [ost-startStop-1] com.category.batch.ServletInitializer : Started ServletInitializer in 17.386 seconds (JVM running for 26.206)

2018-09-14 11:04:23.395 INFO 7008 --- [ main] org.apache.coyote.ajp.AjpNioProtocol : Starting ProtocolHandler ["ajp-nio-8009"]

2018-09-14 11:04:23.399 INFO 7008 --- [ main] org.apache.catalina.startup.Catalina : Server startup in 24866 ms

2018-09-14 11:05:02.889 INFO 7008 --- [ryBean_Worker-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=ReportsJob1]] launched with the following parameters: [{time=1536903301155}]

2018-09-14 11:05:03.262 INFO 7008 --- [ryBean_Worker-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [step]

2018-09-14 11:05:03.503 INFO 7008 --- [ryBean_Worker-1] c.c.batch.reports.tasklet.ReportTasklet : Report's Job is running. Add your business logic here.........

2018-09-14 11:05:03.524 INFO 7008 --- [ryBean_Worker-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=ReportsJob1]] completed with the following parameters: [{time=1536903301155}] and the following status: [COMPLETED]

Жирные линии указывают на то, что ваша работа выполнена и успешно выполнена. Вот и все для этого урока. Пожалуйста, прокомментируйте, если вы хотите что-то добавить. Счастливого обучения!