В моей статье «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 {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@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.
Комментарии (7)
sved
16.11.2019 04:02По своему опыту могу сказать что кварц — штука крайне неудобная, с запутанным API и создаёт кучу непонятных таблиц, которых сложно контролировать.
Как уже выше писали @EnableScheduling работает превосходно. Лок на уровне базы, если надо, делается элементарно. Если требуется динамическое обновление, то это делается в один класс.
В конце концов, можно периодически запускать проверку и через CronSequenceGenerator узнавать нужно ли что либо выполнять в данный момент.lavilav
16.11.2019 10:10А если без базы, то прекрасно локи ставятся через zookeeper
В 2013 писали на кварце приложение большое, что ни месяц — бага на проде. Зависла Джоба.
cepro
17.11.2019 01:45@EnableScheduling работает превосходно. Лок на уровне базы, если надо, делается элементарно.
sved подскажите бест практис, плиз. Заранее благодарю.
sved
17.11.2019 02:31cepro про лок или scheduling?
В спринге есть TaskExecutor и TaskScheduler, которые позволяют programaticaly запускать асинхронные задачи или shсhedul-ить (в том числе и по cron-у)
Им соответствуют аннотации на методы Async и @Scheduled, которые упрощают эту задач, до безобразно простого уровня
stgunholy
Добрый вечер. А почему нельзя обойтись @EnableScheduling вместе с @Scheduled(cron = "* * * * * *")?
DzmitryT
Есть предположение, что для удобства масштабирования сервиса в кластере: Quartz вполре себе умеет держать информацию о задачах и их состоянии в БД. Если же баловаться с аннотациями, то придеться самому думать над тем, как, например, ограничить одновременный запуск задачи на разных нодах. Если где не прав — критику и пояснения приветствую.