Хабрахабр

[Перевод] Добавление Quartz в Spring Boot

И снова здравствуйте. Специально для студентов курса «Разработчик на Spring Framework» подготовили перевод интересной статьи.

В моей статье «Specifications to the Rescue» я показал как можно использовать JPA Specification в Spring Boot для реализации фильтрации в RESTful API. Затем в статье «Testing those Specifications» было показано как протестировать эти самые спецификации.

Следующим шагом я решил продемонстрировать, как добавить планировщик заданий в это же приложение Spring Boot.

Планировщик заданий Quartz

Команда Spring продолжает облегчать разработку на Java, предоставляя различные Spring Boot Starter, подключаемые через простую maven-зависимость.

В этой статье я сконцентрируюсь на стартере Quartz Scheduler, который можно добавить в проект Spring Boot с помощью следующей зависимости:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

Реализация довольно проста и описана здесь. Полный список текущих Spring Boot Starter вы можете посмотреть здесь.

Настройка

Используя работу, опубликованную Дэвидом Киссом, первым этапом будет добавление автосвязывания для заданий Quartz:

public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware @Override protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { final Object job = super.createJobInstance(bundle); beanFactory.autowireBean(job); return job; }
}

Далее добавляем базовую конфигурацию Quartz:

@Configuration
public class QuartzConfig { private ApplicationContext applicationContext; private DataSource dataSource; public QuartzConfig(ApplicationContext applicationContext, DataSource dataSource) { this.applicationContext = applicationContext; this.dataSource = dataSource; } @Bean public SpringBeanJobFactory springBeanJobFactory() { AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); return jobFactory; } @Bean public SchedulerFactoryBean scheduler(Trigger... triggers) { SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); Properties properties = new Properties(); properties.setProperty("org.quartz.scheduler.instanceName", "MyInstanceName"); properties.setProperty("org.quartz.scheduler.instanceId", "Instance1"); schedulerFactory.setOverwriteExistingJobs(true); schedulerFactory.setAutoStartup(true); schedulerFactory.setQuartzProperties(properties); schedulerFactory.setDataSource(dataSource); schedulerFactory.setJobFactory(springBeanJobFactory()); schedulerFactory.setWaitForJobsToCompleteOnShutdown(true); if (ArrayUtils.isNotEmpty(triggers)) { schedulerFactory.setTriggers(triggers); } return schedulerFactory; }
}

Можно вынести свойства, используемые в методе scheduler(), наружу, но я специально решил упростить этот пример.

Затем добавляются статические методы, обеспечивающие программный способ создания заданий и триггеров:

@Slf4j
@Configuration
public class QuartzConfig { ...
static SimpleTriggerFactoryBean createTrigger(JobDetail jobDetail, long pollFrequencyMs, String triggerName) { log.debug("createTrigger(jobDetail={}, pollFrequencyMs={}, triggerName={})", jobDetail.toString(), pollFrequencyMs, triggerName); SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); factoryBean.setJobDetail(jobDetail); factoryBean.setStartDelay(0L); factoryBean.setRepeatInterval(pollFrequencyMs); factoryBean.setName(triggerName); factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT); return factoryBean; } static CronTriggerFactoryBean createCronTrigger(JobDetail jobDetail, String cronExpression, String triggerName) { log.debug("createCronTrigger(jobDetail={}, cronExpression={}, triggerName={})", jobDetail.toString(), cronExpression, triggerName); // To fix an issue with time-based cron jobs Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean(); factoryBean.setJobDetail(jobDetail); factoryBean.setCronExpression(cronExpression); factoryBean.setStartTime(calendar.getTime()); factoryBean.setStartDelay(0L); factoryBean.setName(triggerName); factoryBean.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING); return factoryBean; } static JobDetailFactoryBean createJobDetail(Class jobClass, String jobName) { log.debug("createJobDetail(jobClass={}, jobName={})", jobClass.getName(), jobName); JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); factoryBean.setName(jobName); factoryBean.setJobClass(jobClass); factoryBean.setDurability(true); return factoryBean; }
}

Метод createJobDetail() — это простой и полезный метод для создания заданий.
Для триггеров существуют два варианта: на основе CRON и простые триггеры.

Сервисы

Теперь базовый планировщик Quartz готов к запуску заданий в нашем Spring Boot — приложении. Далее создадим несколько примеров сервисов, которые будут запускаться планировщиком.

Если вы помните, пример в первоначальном проекте был связан с фитнес-клубом. Первый сервис отображает простую статистику членства. В классе MemberService создаем метод memberStats():

public void memberStats() { List<Member> members = memberRepository.findAll(); int activeCount = 0; int inactiveCount = 0; int registeredForClassesCount = 0; int notRegisteredForClassesCount = 0; for (Member member : members) { if (member.isActive()) { activeCount++; if (CollectionUtils.isNotEmpty(member.getMemberClasses())) { registeredForClassesCount++; } else { notRegisteredForClassesCount++; } } else { inactiveCount++; } } log.info("Member Statics:"); log.info("=============="); log.info("Active member count: {}", activeCount); log.info(" - Registered for Classes count: {}", registeredForClassesCount); log.info(" - Not registered for Classes count: {}", notRegisteredForClassesCount); log.info("Inactive member count: {}", inactiveCount); log.info("==========================");
}

Для отслеживания интересов в классах фитнес-клуба создаем в MemberClassService метод classStats():

public void classStats() { List<MemberClass> memberClasses = classRepository.findAll(); Map<String, Integer> memberClassesMap = memberClasses .stream() .collect(Collectors.toMap(MemberClass::getName, c -> 0)); List<Member> members = memberRepository.findAll(); for (Member member : members) { if (CollectionUtils.isNotEmpty(member.getMemberClasses())) { for (MemberClass memberClass : member.getMemberClasses()) { memberClassesMap.merge(memberClass.getName(), 1, Integer::sum); } } } log.info("Class Statics:"); log.info("============="); memberClassesMap.forEach((k,v) -> log.info("{}: {}", k, v)); log.info("==========================");
}

Задания

Для запуска кода сервисов необходимо создать соответствующие задания (Job). Для MemberService я создал класс задания MemberStatsJob:

@Slf4j
@Component
@DisallowConcurrentExecution
public class MemberStatsJob implements Job { @Autowired private MemberService memberService; @Override public void execute(JobExecutionContext context) { log.info("Job ** {} ** starting @ {}", context.getJobDetail().getKey().getName(), context.getFireTime()); memberService.memberStats(); log.info("Job ** {} ** completed. Next job scheduled @ {}", context.getJobDetail().getKey().getName(), context.getNextFireTime()); }
}

Для сервиса MemberClassService был создан класс MemberClassStatsJob:

@Slf4j
@Component
@DisallowConcurrentExecution
public class MemberClassStatsJob implements Job { @Autowired MemberClassService memberClassService; @Override public void execute(JobExecutionContext context) { log.info("Job ** {} ** starting @ {}", context.getJobDetail().getKey().getName(), context.getFireTime()); memberClassService.classStats(); log.info("Job ** {} ** completed. Next job scheduled @ {}", context.getJobDetail().getKey().getName(), context.getNextFireTime()); }
}

Расписание заданий

В этом проекте мы хотим, чтобы все задания были запланированы при запуске Spring Boot сервера. Для этого я создал класс QuartzSubmitJobs, который включает в себя четыре простых метода. Два метода создают новые задания, а два метода — соответствующие триггеры.

@Configuration
public class QuartzSubmitJobs { private static final String CRON_EVERY_FIVE_MINUTES = "0 0/5 * ? * * *"; @Bean(name = "memberStats") public JobDetailFactoryBean jobMemberStats() { return QuartzConfig.createJobDetail(MemberStatsJob.class, "Member Statistics Job"); } @Bean(name = "memberStatsTrigger") public SimpleTriggerFactoryBean triggerMemberStats(@Qualifier("memberStats") JobDetail jobDetail) { return QuartzConfig.createTrigger(jobDetail, 60000, "Member Statistics Trigger"); } @Bean(name = "memberClassStats") public JobDetailFactoryBean jobMemberClassStats() { return QuartzConfig.createJobDetail(MemberClassStatsJob.class, "Class Statistics Job"); } @Bean(name = "memberClassStatsTrigger") public CronTriggerFactoryBean triggerMemberClassStats(@Qualifier("memberClassStats") JobDetail jobDetail) { return QuartzConfig.createCronTrigger(jobDetail, CRON_EVERY_FIVE_MINUTES, "Class Statistics Trigger"); }
}

Запуск Spring Boot

Когда все готово, можно запустить Spring Boot сервер и увидеть инициализацию Quartz:

2019-07-14 14:36:51.651 org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'MyInstanceName' initialized from an externally provided properties instance.
2019-07-14 14:36:51.651 org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.0
2019-07-14 14:36:51.651 org.quartz.core.QuartzScheduler : JobFactory set to: com.gitlab.johnjvester.jpaspec.config.AutowiringSpringBeanJobFactory@79ecc507
2019-07-14 14:36:51.851 o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-07-14 14:36:51.901 aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-07-14 14:36:52.051 o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now
2019-07-14 14:36:52.054 o.s.s.quartz.LocalDataSourceJobStore : Freed 0 triggers from 'acquired' / 'blocked' state.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Recovering 0 jobs that were in-progress at the time of the last shut-down.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Recovery complete.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Removed 0 'complete' triggers.
2019-07-14 14:36:52.058 o.s.s.quartz.LocalDataSourceJobStore : Removed 0 stale fired job entries.
2019-07-14 14:36:52.058 org.quartz.core.QuartzScheduler : Scheduler MyInstanceName_$_Instance1 started.

И запуск задания memberStats():

2019-07-14 14:36:52.096 c.g.j.jpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** starting @ Sun Jul 14 14:36:52 EDT 2019
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Member Statics:
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : ==============
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Active member count: 7
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : - Registered for Classes count: 6
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : - Not registered for Classes count: 1
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Inactive member count: 3
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : ==========================
2019-07-14 14:36:52.219 c.g.j.jpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:37:51 EDT 2019

А затем выполнение задания classStats():

2019-07-14 14:40:00.006 c.g.j.jpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** starting @ Sun Jul 14 14:40:00 EDT 2019
2019-07-14 14:40:00.021 c.g.j.j.service.MemberClassService : Class Statics:
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : =============
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Tennis: 4
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : FitCore 2000: 3
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Spin: 2
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Swimming: 4
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : New Class: 0
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Basketball: 2
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : ==========================
2019-07-14 14:40:00.022 c.g.j.jpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:45:00 EDT 2019

Заключение

В приведенном выше примере я использовал существующий проект на Spring Boot и без особых усилий добавил в него планировщик Quartz. Я создал сервисные методы, которые выполняли простой анализ данных. Эти сервисные методы были запущены классами заданий. Наконец, задания и триггеры были запланированы для запуска.

Полный исходный код можно найти здесь.

В следующей статье я покажу как добавить RESTful API для просмотра информации о настройках Quartz.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть