From 3efed514c3773a1d4e21ddad6c550d5474d5f15f Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 16 May 2024 20:21:36 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=A2=9E=E5=8A=A0Quartz=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1starter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checkstyle/checkstyle-suppressions.xml | 3 +- .../pom.xml | 49 +++ .../quartz/QuartzJobAutoConfiguration.java | 122 +++++++ .../quartz/constants/JobConstant.java | 48 +++ .../quartz/enums/JobExecuteTraceType.java | 28 ++ .../autoconfigure/quartz/enums/JobStage.java | 28 ++ .../quartz/event/JobExecuteTraceEvent.java | 50 +++ .../JobExecuteTraceStoreListener.java | 59 ++++ .../quartz/job/CleanExecuteTraceJob.java | 47 +++ .../quartz/plugin/StoreJobHistoryPlugin.java | 228 +++++++++++++ .../properties/QuartzJobProperties.java | 38 +++ .../quartz/service/QuartzJobService.java | 146 +++++++++ .../service/impl/QuartzJobServiceImpl.java | 307 ++++++++++++++++++ .../quartz/store/JobExecuteTrace.java | 123 +++++++ .../quartz/store/JobExecuteTraceStore.java | 36 ++ .../store/impl/JdbcJobExecuteTraceStore.java | 99 ++++++ .../main/resources/META-INF/spring.factories | 4 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../quartz/JobStartupRunner.java | 92 ++++++ .../autoconfigure/quartz/TestApplication.java | 32 ++ .../autoconfigure/quartz/job/DemoJob.java | 49 +++ .../src/test/resources/application-quartz.yml | 37 +++ .../src/test/resources/application.yml | 6 + pom.xml | 1 + 24 files changed, 1631 insertions(+), 2 deletions(-) create mode 100644 job/ballcat-spring-boot-starter-quartz/pom.xml create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/QuartzJobAutoConfiguration.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/constants/JobConstant.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobExecuteTraceType.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobStage.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/JobExecuteTraceEvent.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/listener/JobExecuteTraceStoreListener.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/job/CleanExecuteTraceJob.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/plugin/StoreJobHistoryPlugin.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/properties/QuartzJobProperties.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/QuartzJobService.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/impl/QuartzJobServiceImpl.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTrace.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTraceStore.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/impl/JdbcJobExecuteTraceStore.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring.factories create mode 100644 job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/JobStartupRunner.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/TestApplication.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/job/DemoJob.java create mode 100644 job/ballcat-spring-boot-starter-quartz/src/test/resources/application-quartz.yml create mode 100644 job/ballcat-spring-boot-starter-quartz/src/test/resources/application.yml diff --git a/ballcat-parent/src/checkstyle/checkstyle-suppressions.xml b/ballcat-parent/src/checkstyle/checkstyle-suppressions.xml index 5f69099b..0049b54c 100644 --- a/ballcat-parent/src/checkstyle/checkstyle-suppressions.xml +++ b/ballcat-parent/src/checkstyle/checkstyle-suppressions.xml @@ -6,8 +6,7 @@ - - + diff --git a/job/ballcat-spring-boot-starter-quartz/pom.xml b/job/ballcat-spring-boot-starter-quartz/pom.xml new file mode 100644 index 00000000..9c8dfbc9 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/pom.xml @@ -0,0 +1,49 @@ + + + + org.ballcat + ballcat-parent + ${revision} + ../../ballcat-parent + + 4.0.0 + ballcat-spring-boot-starter-quartz + + + + org.ballcat + ballcat-common-util + + + org.springframework.boot + spring-boot-starter-quartz + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-data-jdbc + true + + + + + com.mysql + mysql-connector-j + test + + + org.springframework + spring-test + test + + + diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/QuartzJobAutoConfiguration.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/QuartzJobAutoConfiguration.java new file mode 100644 index 00000000..38f2ca83 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/QuartzJobAutoConfiguration.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import lombok.extern.slf4j.Slf4j; +import org.ballcat.autoconfigure.quartz.constants.JobConstant; +import org.ballcat.autoconfigure.quartz.event.listener.JobExecuteTraceStoreListener; +import org.ballcat.autoconfigure.quartz.job.CleanExecuteTraceJob; +import org.ballcat.autoconfigure.quartz.properties.QuartzJobProperties; +import org.ballcat.autoconfigure.quartz.service.QuartzJobService; +import org.ballcat.autoconfigure.quartz.service.impl.QuartzJobServiceImpl; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTraceStore; +import org.ballcat.autoconfigure.quartz.store.impl.JdbcJobExecuteTraceStore; +import org.quartz.Scheduler; +import org.quartz.spi.SchedulerPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.quartz.QuartzProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * quartz 自动装配 + * + * @author evil0th Create on 2024/5/8 + */ +@Slf4j +@EnableAsync +@AutoConfiguration +@AutoConfigureAfter(QuartzAutoConfiguration.class) +@EnableConfigurationProperties(QuartzJobProperties.class) +@ConditionalOnProperty(prefix = QuartzJobProperties.PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class QuartzJobAutoConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(QuartzJobAutoConfiguration.class); + + @Bean + public QuartzJobService quartzJobService(Scheduler scheduler) { + return new QuartzJobServiceImpl(scheduler); + } + + /** + * 配置了插件则响应存储组件要初始化 同时添加清理历史数据的任务 + * 下游可以实现{@link JobExecuteTraceStore}接口和{@link SchedulerPlugin}来自定义存储 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnSingleCandidate(DataSource.class) + @ConditionalOnProperty(prefix = "spring.quartz", name = "job-store-type", havingValue = "jdbc") + static class JobExecuteTraceStoreConfiguration { + + /** + * 提供默认jdbc存储实现 + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_PROPERTIES_PREFIX, name = "class", + havingValue = JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_CLASS) + public JobExecuteTraceStore jdbcJobExecuteTraceStore(DataSource dataSource) { + return new JdbcJobExecuteTraceStore(dataSource); + } + + /** + * 定制了清理 cron 配置 + */ + @Bean + @ConditionalOnBean(JobExecuteTraceStore.class) + @ConditionalOnProperty(prefix = JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_PROPERTIES_PREFIX, + name = "cleanCron") + public JobExecuteTraceStoreListener jobExecuteTraceStoreListener(JobExecuteTraceStore jobExecuteTraceStore, + QuartzJobService quartzJobService, QuartzProperties quartzProperties) { + // 按Cron表达式执行清理历史数据Job + final String cron = quartzProperties.getProperties() + .get(JobConstant.PLUGIN_PROPERTIES_PREFIX + "." + JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME + + ".cleanCron"); + // 默认清理30天以前的历史数据 + final int cleanDays = Integer.parseInt(quartzProperties.getProperties() + .getOrDefault(JobConstant.PLUGIN_PROPERTIES_PREFIX + "." + + JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME + ".cleanDays", "30")); + Map params = new HashMap<>(); + params.put("cleanDays", cleanDays); + Date latest = quartzJobService.addJob(CleanExecuteTraceJob.class, + JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME + "CleanJob", JobConstant.RESERVED_JOB_GROUP, + params, cron, null); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[定时清理任务{}天前历史记录]初始化, CRON={}, 下一次任务执行的开始时间{}", cleanDays, cron, latest); + } + return new JobExecuteTraceStoreListener(jobExecuteTraceStore); + } + + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/constants/JobConstant.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/constants/JobConstant.java new file mode 100644 index 00000000..481072a8 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/constants/JobConstant.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.constants; + +/** + * Quartz 常量 + * + * @author evil0th Create on 2024/5/13 + */ +public final class JobConstant { + + private JobConstant() { + } + + /** + * 保留Job分组 + */ + public static final String RESERVED_JOB_GROUP = "ReservedJobGroup"; + + public static final String QUARTZ_PROPERTIES_PREFIX = "spring.quartz.properties"; + + /** + * quartz插件配置前缀 + */ + public static final String PLUGIN_PROPERTIES_PREFIX = "org.quartz.plugin"; + + public static final String DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME = "storeJobHistory"; + + public static final String DEFAULT_STORE_JOB_HISTORY_PLUGIN_PROPERTIES_PREFIX = QUARTZ_PROPERTIES_PREFIX + "." + + PLUGIN_PROPERTIES_PREFIX + "." + DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME; + + public static final String DEFAULT_STORE_JOB_HISTORY_PLUGIN_CLASS = "org.ballcat.autoconfigure.quartz.plugin.StoreJobHistoryPlugin"; + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobExecuteTraceType.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobExecuteTraceType.java new file mode 100644 index 00000000..a25e6172 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobExecuteTraceType.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.enums; + +/** + * 任务执行记录事件类型 + * + * @author evil0th Create on 2024/5/8 + */ +public enum JobExecuteTraceType { + + INITIALIZE, UPDATE + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobStage.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobStage.java new file mode 100644 index 00000000..dc09eaf9 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/enums/JobStage.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.enums; + +/** + * 任务执行记录阶段 + * + * @author evil0th Create on 2024/5/8 + */ +public enum JobStage { + + EXECUTING, FINISHED, EXCEPTION + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/JobExecuteTraceEvent.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/JobExecuteTraceEvent.java new file mode 100644 index 00000000..fa77a7cf --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/JobExecuteTraceEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.event; + +import lombok.Getter; +import lombok.Setter; +import org.ballcat.autoconfigure.quartz.enums.JobExecuteTraceType; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTrace; +import org.springframework.context.ApplicationEvent; + +/** + * 任务执行记录事件 + * + * @author evil0th Create on 2024/5/8 + */ +@Getter +@Setter +public class JobExecuteTraceEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + private final JobExecuteTrace jobExecuteTrace; + + private JobExecuteTraceType eventType; + + /** + * @param jobExecuteTrace JobExecuteTrace + * @param eventType {@link JobExecuteTraceType} + */ + public JobExecuteTraceEvent(JobExecuteTrace jobExecuteTrace, JobExecuteTraceType eventType) { + super(jobExecuteTrace.getJobName()); + this.jobExecuteTrace = jobExecuteTrace; + this.eventType = eventType; + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/listener/JobExecuteTraceStoreListener.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/listener/JobExecuteTraceStoreListener.java new file mode 100644 index 00000000..cc0e06b8 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/event/listener/JobExecuteTraceStoreListener.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.event.listener; + +import lombok.RequiredArgsConstructor; +import org.ballcat.autoconfigure.quartz.event.JobExecuteTraceEvent; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTraceStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +/** + * 任务执行记录事件监听 + * + * @author evil0th Create on 2024/5/8 + */ +@RequiredArgsConstructor +public class JobExecuteTraceStoreListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(JobExecuteTraceStoreListener.class); + + private final JobExecuteTraceStore jobExecuteTraceStore; + + /** + * 处理异步保存任务执行记录事件 + * @param event {@link JobExecuteTraceEvent} + */ + @Async + @EventListener(JobExecuteTraceEvent.class) + public void handleJobExecuteTraceEvent(JobExecuteTraceEvent event) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("接收到异步存储任务执行记录事件{},处理中...", event); + } + switch (event.getEventType()) { + case UPDATE: + case INITIALIZE: + this.jobExecuteTraceStore.storeTrace(event.getJobExecuteTrace()); + break; + default: + LOGGER.error("未知的事件类型:{}", event.getEventType()); + } + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/job/CleanExecuteTraceJob.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/job/CleanExecuteTraceJob.java new file mode 100644 index 00000000..0b9a833a --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/job/CleanExecuteTraceJob.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.job; + +import lombok.RequiredArgsConstructor; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTraceStore; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.lang.NonNull; +import org.springframework.scheduling.quartz.QuartzJobBean; + +/** + * 清理 {@link JobExecuteTraceStore} 中的记录的历史数据Job + * + * @author evil0th Create on 2024/5/8 + */ +@RequiredArgsConstructor +@PersistJobDataAfterExecution +@DisallowConcurrentExecution +public class CleanExecuteTraceJob extends QuartzJobBean { + + final JobExecuteTraceStore jobExecuteTraceStore; + + @Override + protected void executeInternal(@NonNull JobExecutionContext context) { + JobDataMap params = context.getMergedJobDataMap(); + final int cleanDays = params.getInt("cleanDays"); + this.jobExecuteTraceStore.cleanUp(cleanDays); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/plugin/StoreJobHistoryPlugin.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/plugin/StoreJobHistoryPlugin.java new file mode 100644 index 00000000..6a2b011b --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/plugin/StoreJobHistoryPlugin.java @@ -0,0 +1,228 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.plugin; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import org.ballcat.autoconfigure.quartz.constants.JobConstant; +import org.ballcat.autoconfigure.quartz.enums.JobExecuteTraceType; +import org.ballcat.autoconfigure.quartz.enums.JobStage; +import org.ballcat.autoconfigure.quartz.event.JobExecuteTraceEvent; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTrace; +import org.ballcat.common.util.SpringUtils; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronTrigger; +import org.quartz.DailyTimeIntervalTrigger; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.JobListener; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; +import org.quartz.impl.matchers.EverythingMatcher; +import org.quartz.impl.triggers.SimpleTriggerImpl; +import org.quartz.spi.ClassLoadHelper; +import org.quartz.spi.SchedulerPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 保存任务历史记录 + * + * @author evil0th Create on 2024/5/8 + */ + +@Setter +@Getter +public class StoreJobHistoryPlugin implements SchedulerPlugin, JobListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(StoreJobHistoryPlugin.class); + + /** + * 清理历史执行记录的Cron表达式。只是为了properties接收插件配置,无其他作用。 + */ + private String cleanCron; + + /** + * 历史执行记录保留天数。只是为了properties接收插件配置,无其他作用。 + */ + private String cleanDays; + + @Override + public String getName() { + return JobConstant.DEFAULT_STORE_JOB_HISTORY_PLUGIN_NAME; + } + + @Override + public void jobToBeExecuted(JobExecutionContext context) { + JobDetail jobDetail = context.getJobDetail(); + JobKey jobKey = jobDetail.getKey(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[自定义插件:{}][{}]开始保存准备执行记录.", getName(), jobKey.getName()); + } + try { + triggerLog(context, null, JobExecuteTraceType.INITIALIZE); + } + catch (Exception e) { + LOGGER.error("[自定义插件:{}][{}]保存准备执行记录失败.", getName(), jobKey.getName(), e); + } + } + + @Override + public void jobExecutionVetoed(JobExecutionContext context) { + + } + + /** + * 执行完自定义插件 + */ + @Override + public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { + JobDetail jobDetail = context.getJobDetail(); + JobKey jobKey = jobDetail.getKey(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[自定义插件:{}][{}]开始保存执行完毕记录.", getName(), jobKey.getName()); + } + try { + triggerLog(context, jobException, JobExecuteTraceType.UPDATE); + } + catch (Exception e) { + LOGGER.error("[自定义插件:{}][{}]保存执行完毕记录失败.", getName(), jobKey.getName(), e); + } + } + + /** + * 初始化自定义插件 + */ + @Override + public void initialize(String name, Scheduler scheduler, ClassLoadHelper loadHelper) throws SchedulerException { + scheduler.getListenerManager().addJobListener(this, EverythingMatcher.allJobs()); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[自定义插件:{}]初始化.", getName()); + } + } + + /** + * 启动自定义插件 + */ + @Override + public void start() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[自定义插件:{}]启动.", getName()); + } + } + + /** + * 关闭自定义插件 + */ + @Override + public void shutdown() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("[自定义插件:{}]关闭.", getName()); + } + } + + private void triggerLog(JobExecutionContext context, JobExecutionException jobException, + JobExecuteTraceType traceType) { + Trigger trigger = context.getTrigger(); + JobDetail jobDetail = context.getJobDetail(); + JobKey jobKey = jobDetail.getKey(); + JobExecuteTrace trace = new JobExecuteTrace().setJobGroup(jobKey.getGroup()) + .setJobName(jobKey.getName()) + .setJobDescription(jobDetail.getDescription()) + .setTriggerType(trigger.getClass().getSimpleName()) + .setJobStrategy(getTriggerStrategy(trigger)) + .setJobClass(jobDetail.getJobClass().getName()) + .setJobParams(getJobDataMapJsonString(trigger.getJobDataMap())) + .setExecuteTime(convertDate(context.getFireTime())) + .setInstanceName(context.getFireInstanceId()) + .setStartTime(convertDate(trigger.getStartTime())) + .setEndTime(convertDate(trigger.getEndTime())) + .setPreviousFireTime(convertDate(trigger.getPreviousFireTime())) + .setNextFireTime(convertDate(trigger.getNextFireTime())); + switch (traceType) { + case INITIALIZE: + trace.setExecuteState(JobStage.EXECUTING.name()); + break; + case UPDATE: + trace.setRunTime(context.getJobRunTime()) + .setRetryTimes(context.getRefireCount()) + .setExecuteState(jobException == null ? JobStage.FINISHED.name() : JobStage.EXCEPTION.name()) + .setMessage(jobException != null ? jobException.getMessage() : null); + break; + } + SpringUtils.publishEvent(new JobExecuteTraceEvent(trace, traceType)); + } + + /** + * @param trigger 触发器 + * @return string + * @see SimpleTrigger SimpleTrigger:简单触发器 固定时刻或时间间隔,毫秒 + * @see CalendarIntervalTrigger CalendarIntervalTrigger:基于日历的触发器 + * 比简单触发器更多时间单位,支持非固定时间的触发,例如一年可能 365/366,一个月可能 28/29/30/31 + * @see DailyTimeIntervalTrigger DailyTimeIntervalTrigger:基于日期的触发器 每天的某个时间段 + * @see CronTrigger CronTrigger:基于 Cron 表达式的触发器 + */ + private String getTriggerStrategy(Trigger trigger) { + if (null == trigger) { + return null; + } + if (trigger instanceof SimpleTriggerImpl) { + // 这里也不是绝对的,依赖于QuartzService在创建未来时间执行使用的API + SimpleTriggerImpl simpleTrigger = (SimpleTriggerImpl) trigger; + final LocalDateTime localDateTime = convertDate(simpleTrigger.getStartTime()); + return localDateTime == null ? null : localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + else if (trigger instanceof CronTrigger) { + return ((CronTrigger) trigger).getCronExpression(); + } + else if (trigger instanceof DailyTimeIntervalTrigger) { + DailyTimeIntervalTrigger intervalTrigger = (DailyTimeIntervalTrigger) trigger; + return String.format("%d/%s", intervalTrigger.getRepeatInterval(), intervalTrigger.getRepeatIntervalUnit()); + } + else if (trigger instanceof CalendarIntervalTrigger) { + CalendarIntervalTrigger intervalTrigger = (CalendarIntervalTrigger) trigger; + return String.format("%d/%s", intervalTrigger.getRepeatInterval(), intervalTrigger.getRepeatIntervalUnit()); + } + return null; + } + + private LocalDateTime convertDate(Date dateToConvert) { + if (null == dateToConvert) { + return null; + } + return LocalDateTime.ofInstant(dateToConvert.toInstant(), ZoneId.systemDefault()); + } + + private String getJobDataMapJsonString(JobDataMap map) { + if (null == map) { + return null; + } + return map.keySet().stream().map(key -> key + ":" + map.get(key)).collect(Collectors.joining(",", "{", "}")); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/properties/QuartzJobProperties.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/properties/QuartzJobProperties.java new file mode 100644 index 00000000..2ea4b56c --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/properties/QuartzJobProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author evil0th Create on 2024/5/8 + */ +@Getter +@Setter +@ConfigurationProperties(QuartzJobProperties.PREFIX) +public class QuartzJobProperties { + + public static final String PREFIX = "ballcat.quartz.job"; + + /** + * 是否启用Quartz调度任务,默认:开启 + */ + private boolean enabled = true; + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/QuartzJobService.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/QuartzJobService.java new file mode 100644 index 00000000..090f59bf --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/QuartzJobService.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.service; + +import java.util.Date; +import java.util.Map; + +import org.quartz.DateBuilder; +import org.quartz.Job; + +/** + * quartz service + * + * @author evil0th Create on 2024/5/8 + */ +public interface QuartzJobService { + + /** + * 以指定计划时间的方式新增一个任务 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param scheduleTime 指定schedule时间执行 + * @param desc 任务描述 + * @return 最近一次任务执行的开始时间 + */ + Date addJob(Class clazz, String name, String group, Map params, Date scheduleTime, + String desc); + + /** + * 以指定未来多少时间的方式新增一个任务 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param future 未来多少时间 + * @param unit 时间单位 + * @param desc 任务描述 + * @return 最近一次任务执行的开始时间 + */ + Date addJob(Class clazz, String name, String group, Map params, int future, + DateBuilder.IntervalUnit unit, String desc); + + /** + * 以指定未来多少秒的方式新增一个任务 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param future 未来多少时间 + * @param desc 任务描述 + * @return 最近一次任务执行的开始时间 + */ + Date addJob(Class clazz, String name, String group, Map params, int future, + String desc); + + /** + * 以cron的方式新增一个任务 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param cronExpression CRON表达式 + * @param desc 任务描述 + * @return 最近一次任务执行的开始时间 + */ + Date addJob(Class clazz, String name, String group, Map params, + String cronExpression, String desc); + + /** + * 以cron的方式新增一个任务并指定结束时间 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param cronExpression CRON表达式 + * @param desc 任务描述 + * @param endTime 任务结束时间 + * @return 最近一次任务执行的开始时间 + */ + Date addJob(Class clazz, String name, String group, Map params, + String cronExpression, String desc, Date endTime); + + /** + * 添加任务每隔多少秒执行一次 + * @param clazz 任务实现类 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param desc 任务描述 + * @param startTime 任务开始时间 + * @param endTime 任务结束时间 + * @param interval 间隔秒数 + * @return 最近一次任务执行的开始时间 + */ + Date addJobWithIntervalInSeconds(Class clazz, String name, String group, Map params, + String desc, Date startTime, Date endTime, int interval); + + /** + * 删除任务 + * @param name 任务名称 + * @param group 任务分组 + * @return 是否删除 + */ + boolean deleteJob(String name, String group); + + /** + * 是否存在Job + * @param name 任务名称 + * @param group 任务分组 + * @return 是否存在 + */ + boolean existsJob(String name, String group); + + /** + * 更改任务参数、计划执行时间 + * @param name 任务名称 + * @param group 任务分组 + * @param params 任务参数 + * @param scheduleTime 指定呢计划执行时间 + * @return 最近一次任务执行的开始时间 + */ + Date modifyJob(String name, String group, Map params, Date scheduleTime); + + /** + * 删除任务组 + * @param group 任务分组 + */ + void deleteGroupJobs(String group); + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/impl/QuartzJobServiceImpl.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/impl/QuartzJobServiceImpl.java new file mode 100644 index 00000000..993ddcd8 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/service/impl/QuartzJobServiceImpl.java @@ -0,0 +1,307 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.service.impl; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.ballcat.autoconfigure.quartz.service.QuartzJobService; +import org.quartz.CalendarIntervalScheduleBuilder; +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.DateBuilder; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * quartz service impl + * + * @author evil0th Create on 2024/5/8 + */ +@RequiredArgsConstructor +public class QuartzJobServiceImpl implements QuartzJobService { + + private static final Logger LOGGER = LoggerFactory.getLogger(QuartzJobServiceImpl.class); + + /** + * 优先级在此维护 + *

+ * 较高优先级的触发器先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5; + */ + public static final int PRIORITY_DEFAULT = 5; + + public static final String CRON_FORMAT = "ss mm HH dd MM ? yyyy"; + + final Scheduler scheduler; + + @Override + public Date addJob(Class clazz, String name, String group, Map params, + Date scheduleTime, String desc) { + try { + // 先检查任务是否存在,存在则删除 + JobKey jobKey = JobKey.jobKey(name, group); + if (this.scheduler.checkExists(jobKey)) { + deleteJob(name, group); + } + JobDetail job = JobBuilder.newJob(clazz).withDescription(desc).withIdentity(jobKey).build(); + if (params == null) { + params = new HashMap<>(); + } + JobDataMap dataMap = new JobDataMap(params); + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + // 创建触发器 + SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withMisfireHandlingInstructionFireNow(); + TriggerBuilder builder = TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .usingJobData(dataMap) + .withDescription(desc) + .startAt(scheduleTime) + .withPriority(PRIORITY_DEFAULT); + return this.scheduler.scheduleJob(job, builder.withSchedule(simpleScheduleBuilder).build()); + } + catch (Exception e) { + LOGGER.error("以指定计划时间的方式新增任务失败!", e); + throw new RuntimeException(e); + } + } + + @Override + public Date addJob(Class clazz, String name, String group, Map params, int future, + DateBuilder.IntervalUnit unit, String desc) { + try { + // 先检查任务是否存在,存在则删除 + JobKey jobKey = JobKey.jobKey(name, group); + if (this.scheduler.checkExists(jobKey)) { + deleteJob(name, group); + } + JobDetail job = JobBuilder.newJob(clazz).withDescription(desc).withIdentity(jobKey).build(); + if (params == null) { + params = new HashMap<>(); + } + JobDataMap dataMap = new JobDataMap(params); + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + // 创建触发器 + SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withMisfireHandlingInstructionFireNow(); + TriggerBuilder builder = TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .usingJobData(dataMap) + .withDescription(desc) + .startAt(DateBuilder.futureDate(future, null == unit ? DateBuilder.IntervalUnit.SECOND : unit)) + .withPriority(PRIORITY_DEFAULT); + return this.scheduler.scheduleJob(job, builder.withSchedule(simpleScheduleBuilder).build()); + } + catch (Exception e) { + LOGGER.error("以指定计划时间的方式新增任务失败!", e); + throw new RuntimeException(e); + } + } + + @Override + public Date addJob(Class clazz, String name, String group, Map params, int future, + String desc) { + return addJob(clazz, name, group, params, future, DateBuilder.IntervalUnit.SECOND, desc); + } + + @Override + public Date addJob(Class clazz, String name, String group, Map params, + String cronExpression, String desc) { + return addJob(clazz, name, group, params, cronExpression, desc, new Date(), null); + } + + @Override + public Date addJob(Class clazz, String name, String group, Map params, + String cronExpression, String desc, Date endTime) { + return addJob(clazz, name, group, params, cronExpression, desc, new Date(), endTime); + } + + public Date addJob(Class clazz, String name, String group, Map params, + String cronExpression, String desc, Date startTime, Date endTime) { + try { + // 先检查任务是否存在,存在则删除 + JobKey jobKey = JobKey.jobKey(name, group); + if (this.scheduler.checkExists(jobKey)) { + deleteJob(name, group); + } + JobDetail job = JobBuilder.newJob(clazz).withDescription(desc).withIdentity(jobKey).build(); + if (params == null) { + params = new HashMap<>(); + } + JobDataMap dataMap = new JobDataMap(params); + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + // 创建触发器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression) + .withMisfireHandlingInstructionFireAndProceed(); + TriggerBuilder builder = TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .usingJobData(dataMap) + .withDescription(desc) + .startAt(startTime) + .withPriority(PRIORITY_DEFAULT); + if (endTime != null) { + builder = builder.endAt(endTime); + } + return this.scheduler.scheduleJob(job, builder.withSchedule(cronScheduleBuilder).build()); + } + catch (Exception e) { + LOGGER.error("以cron的方式新增任务失败!", e); + throw new RuntimeException(e); + } + } + + @Override + public Date addJobWithIntervalInSeconds(Class clazz, String name, String group, + Map params, String desc, Date startTime, Date endTime, int interval) { + try { + // 先检查任务是否存在,存在则删除 + JobKey jobKey = JobKey.jobKey(name, group); + if (this.scheduler.checkExists(jobKey)) { + deleteJob(name, group); + } + JobDetail job = JobBuilder.newJob(clazz).withDescription(desc).withIdentity(jobKey).build(); + if (params == null) { + params = new HashMap<>(); + } + JobDataMap dataMap = new JobDataMap(params); + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + // 创建触发器 + CalendarIntervalScheduleBuilder cisb = CalendarIntervalScheduleBuilder.calendarIntervalSchedule() + .withIntervalInSeconds(interval) + .withMisfireHandlingInstructionFireAndProceed(); + TriggerBuilder builder = TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .usingJobData(dataMap) + .withDescription(desc) + .startAt(startTime) + .withPriority(PRIORITY_DEFAULT); + if (endTime != null) { + builder = builder.endAt(endTime); + } + return this.scheduler.scheduleJob(job, builder.withSchedule(cisb).build()); + } + catch (Exception e) { + LOGGER.error("以每隔多少秒执行一次方式新增任务失败!", e); + throw new RuntimeException(e); + } + } + + @Override + public boolean deleteJob(String name, String group) { + try { + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + // 停止触发器 + this.scheduler.pauseTrigger(triggerKey); + // 移除触发器 + this.scheduler.unscheduleJob(triggerKey); + JobKey jobKey = JobKey.jobKey(name, group); + // 删除任务 + return this.scheduler.deleteJob(jobKey); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean existsJob(String name, String group) { + try { + // 先检查任务是否存在,存在则删除 + JobKey jobKey = JobKey.jobKey(name, group); + return this.scheduler.checkExists(jobKey); + } + catch (Exception e) { + LOGGER.error("检测任务出错错误!", e); + } + return false; + } + + @Override + public Date modifyJob(String name, String group, Map params, Date scheduleTime) { + try { + TriggerKey triggerKey = TriggerKey.triggerKey(name, group); + CronTrigger trigger = (CronTrigger) this.scheduler.getTrigger(triggerKey); + if (trigger == null) { + return null; + } + String oldTime = trigger.getCronExpression(); + String newTime = LocalDateTime.ofInstant(scheduleTime.toInstant(), ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern(CRON_FORMAT)); + if (!newTime.equals(oldTime)) { + JobKey jobKey = JobKey.jobKey(name, group); + JobDetail jobDetail = this.scheduler.getJobDetail(jobKey); + if (jobDetail != null) { + // 修改时间 + if (deleteJob(name, group)) { + return addJob(jobDetail.getJobClass(), name, group, params, scheduleTime, + jobDetail.getDescription()); + } + } + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + return null; + } + + /** + * 根据组名称删除任务组 + * @param group 任务组 + */ + @Override + public void deleteGroupJobs(String group) { + try { + GroupMatcher triggerMatcher = GroupMatcher.groupEquals(group); + Set triggerKeySet = this.scheduler.getTriggerKeys(triggerMatcher); + List triggerKeyList = new ArrayList<>(triggerKeySet); + this.scheduler.pauseTriggers(triggerMatcher); + this.scheduler.unscheduleJobs(triggerKeyList); + GroupMatcher matcher = GroupMatcher.groupEquals(group); + Set jobKeySet = this.scheduler.getJobKeys(matcher); + List jobKeyList = new ArrayList<>(jobKeySet); + this.scheduler.deleteJobs(jobKeyList); + // 要恢复组触发器,否则以后添加的任务会处于暂停状态,无法运行 + this.scheduler.resumeTriggers(triggerMatcher); + } + catch (Exception e) { + LOGGER.error("删除任务组失败!", e); + throw new RuntimeException(e); + } + + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTrace.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTrace.java new file mode 100644 index 00000000..6fb62dbe --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTrace.java @@ -0,0 +1,123 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.store; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 任务执行记录 + * + * @author evil0th Create on 2024/5/8 + */ +@Data +@Accessors(chain = true) +public class JobExecuteTrace implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + /** + * 分组名称 + */ + private String jobGroup; + + /** + * 任务名称 + */ + private String jobName; + + /** + * 任务描述 + */ + private String jobDescription; + + /** + * 任务类型 + */ + private String triggerType; + + /** + * 执行策略 + */ + private String jobStrategy; + + /** + * 任务类 + */ + private String jobClass; + + /** + * 任务参数 + */ + private String jobParams; + + /** + * 执行时间 + */ + private LocalDateTime executeTime; + + /** + * 执行结果 + */ + private String executeState; + + /** + * 执行时间 + */ + private Long runTime; + + /** + * 重试次数 + */ + private Integer retryTimes; + + /** + * 上一次执行时间 + */ + private LocalDateTime previousFireTime; + + /** + * 下一次执行时间 + */ + private LocalDateTime nextFireTime; + + /** + * 任务消息 + */ + private String message; + + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 结束时间 + */ + private LocalDateTime endTime; + + /** + * 实例名称 + */ + private String instanceName; + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTraceStore.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTraceStore.java new file mode 100644 index 00000000..f1062aef --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/JobExecuteTraceStore.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.store; + +/** + * 执行记录持久化接口 + * + * @author evil0th Create on 2024/5/8 + */ +public interface JobExecuteTraceStore { + + /** + * Store trace + */ + void storeTrace(JobExecuteTrace jobExecuteTrace); + + /** + * clean history + */ + void cleanUp(int cleanDays); + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/impl/JdbcJobExecuteTraceStore.java b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/impl/JdbcJobExecuteTraceStore.java new file mode 100644 index 00000000..3d53f6af --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/java/org/ballcat/autoconfigure/quartz/store/impl/JdbcJobExecuteTraceStore.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.store.impl; + +import java.sql.Types; +import java.time.LocalDateTime; + +import javax.sql.DataSource; + +import lombok.Getter; +import lombok.Setter; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTrace; +import org.ballcat.autoconfigure.quartz.store.JobExecuteTraceStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.Assert; + +/** + * 默认jdbc存储实现 + * + *

+ * 用户可自行实现性能更好的存储方式,如接入监控系统等 + *

+ * + * @author evil0th Create on 2024/5/8 + */ +@Getter +@Setter +public class JdbcJobExecuteTraceStore implements JobExecuteTraceStore { + + private static final Logger LOGGER = LoggerFactory.getLogger(JdbcJobExecuteTraceStore.class); + + private static final String TABLE_NAME = "qrtz_job_execute_trace"; + + private static final String DEFAULT_TRACE_INSERT_STATEMENT = "insert into " + TABLE_NAME + + "(`job_group`, `job_name`, `job_description`, `trigger_type`, " + + "`job_strategy`, `job_class`, `job_params`, `execute_time`, `execute_state`, " + + "`run_time`, `retry_times`, `previous_fire_time`, `next_fire_time`, " + + "`message`, `start_time`, `end_time`, `instance_name`) " + + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + private static final String DEFAULT_HISTORY_TRACE_DELETE_STATEMENT = "delete from " + TABLE_NAME + + " where create_time < ?"; + + private String insertTraceSql = DEFAULT_TRACE_INSERT_STATEMENT; + + private String deleteHistoryTraceSql = DEFAULT_HISTORY_TRACE_DELETE_STATEMENT; + + private final JdbcTemplate jdbcTemplate; + + private final DataSource dataSource; + + public JdbcJobExecuteTraceStore(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource required"); + this.dataSource = dataSource; + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public void storeTrace(JobExecuteTrace t) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("storeTrace:{}", t); + } + this.jdbcTemplate.update(this.insertTraceSql, + new Object[] { t.getJobGroup(), t.getJobName(), t.getJobDescription(), t.getTriggerType(), + t.getJobStrategy(), t.getJobClass(), t.getJobParams(), t.getExecuteTime(), t.getExecuteState(), + t.getRunTime(), t.getRetryTimes(), t.getPreviousFireTime(), t.getNextFireTime(), t.getMessage(), + t.getStartTime(), t.getEndTime(), t.getInstanceName() }, + new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, + Types.VARCHAR, Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER, Types.INTEGER, Types.TIMESTAMP, + Types.TIMESTAMP, Types.VARCHAR, Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR }); + + } + + @Override + public void cleanUp(int cleanDays) { + if (LOGGER.isDebugEnabled()) { + LOGGER.info("清理{}天之前的任务历史记录 ...", cleanDays); + } + this.jdbcTemplate.update(this.deleteHistoryTraceSql, new Object[] { LocalDateTime.now().minusDays(cleanDays) }, + new int[] { Types.TIMESTAMP }); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring.factories b/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..de7e3f42 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.ballcat.autoconfigure.quartz.QuartzJobAutoConfiguration + + diff --git a/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..b4a3d3ee --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.ballcat.autoconfigure.quartz.QuartzJobAutoConfiguration \ No newline at end of file diff --git a/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/JobStartupRunner.java b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/JobStartupRunner.java new file mode 100644 index 00000000..30808c76 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/JobStartupRunner.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import lombok.RequiredArgsConstructor; +import org.ballcat.autoconfigure.quartz.job.DemoJob; +import org.ballcat.autoconfigure.quartz.service.QuartzJobService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * 初始化job + * + * @author evil0th Create on 2024/5/8 + */ +@RequiredArgsConstructor +@Component +public class JobStartupRunner implements CommandLineRunner { + + private static final Logger LOGGER = LoggerFactory.getLogger(JobStartupRunner.class); + + final QuartzJobService quartzJobService; + + @Override + public void run(String... args) { + initIntervalJob(); + initCronJob(); + initOneTimeJob(); + } + + /** + * 初始化周期性定时任务 + */ + public void initIntervalJob() { + int interval = 30; + Map params = new HashMap<>(); + params.put("jobType", "interval"); + params.put("paramLong", 1L); + params.put("paramBoolean", Boolean.TRUE); + Date latest = this.quartzJobService.addJobWithIntervalInSeconds(DemoJob.class, "DemoIntervalJob", + "DemoJobGroup", params, String.format("每%d秒执行Job", interval), new Date(), null, interval); + LOGGER.info("[周期间隔Job]初始化完成,每隔{}秒执行一次,下一次任务执行的开始时间{}", interval, latest); + } + + /** + * 初始化cron定时任务 + */ + public void initCronJob() { + String cron = "0 0/1 * * * ?"; + Map params = new HashMap<>(); + params.put("jobType", "cron"); + params.put("paramLong", 2L); + params.put("paramBoolean", Boolean.TRUE); + Date latest = this.quartzJobService.addJob(DemoJob.class, "DemoCronJob", "DemoJobGroup", params, cron, + String.format("Cron[%s] Job", cron)); + LOGGER.info("[Cron Job]初始化完成,cron:{},下一次任务执行的开始时间{}", cron, latest); + } + + /** + * 初始化一次性任务 + */ + private void initOneTimeJob() { + Map params = new HashMap<>(); + params.put("jobType", "OneTime"); + params.put("paramLong", 3L); + params.put("paramBoolean", Boolean.TRUE); + Date latest = this.quartzJobService.addJob(DemoJob.class, "DemoOneTimeJob", "DemoJobGroup", params, 5, + "一次性Job"); + LOGGER.info("[一次性Job]初始化完成,下一次任务执行的开始时间{}", latest); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/TestApplication.java b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/TestApplication.java new file mode 100644 index 00000000..c16adf84 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/TestApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author evil0th Create on 2024/5/13 + */ +@SpringBootApplication(scanBasePackageClasses = { org.ballcat.common.util.SpringUtils.class }) +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/job/DemoJob.java b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/job/DemoJob.java new file mode 100644 index 00000000..b37c1dc3 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/test/java/org/ballcat/autoconfigure/quartz/job/DemoJob.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ballcat.autoconfigure.quartz.job; + +import lombok.RequiredArgsConstructor; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.PersistJobDataAfterExecution; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.quartz.QuartzJobBean; + +/** + * 周期间隔Job + * + * @author evil0th Create on 2024/5/8 + */ +@RequiredArgsConstructor +@PersistJobDataAfterExecution +@DisallowConcurrentExecution +public class DemoJob extends QuartzJobBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(DemoJob.class); + + @Override + public void executeInternal(JobExecutionContext context) { + JobDataMap params = context.getMergedJobDataMap(); + String jobType = params.getString("jobType"); + long paramLong = params.getLong("paramLong"); + boolean paramBoolean = params.getBooleanValue("paramBoolean"); + LOGGER.info("准备执行Job,参数jobType={},long={},boolean={}", jobType, paramLong, paramBoolean); + } + +} diff --git a/job/ballcat-spring-boot-starter-quartz/src/test/resources/application-quartz.yml b/job/ballcat-spring-boot-starter-quartz/src/test/resources/application-quartz.yml new file mode 100644 index 00000000..b95599c2 --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/test/resources/application-quartz.yml @@ -0,0 +1,37 @@ +spring: + datasource: + url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:demo}?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: ${MYSQL_USERNAME:root} + password: ${MYSQL_PASSWORD:123456} + quartz: + job-store-type: jdbc + properties: + org: + quartz: + jobStore: + clusterCheckinInterval: 5000 + isClustered: true + tablePrefix: QRTZ_ + useProperties: false + plugin: + # 任务执行记录插件(Ballcat提供,可自行实现数据库或监控接入实现) + storeJobHistory: + class: org.ballcat.autoconfigure.quartz.plugin.StoreJobHistoryPlugin + cleanCron: 0 0 10 * * ? + cleanDays: 5 + # 任务历史记录(官方提供,仅日志输出) +# jobHistory: +# class: org.quartz.plugins.history.LoggingJobHistoryPlugin +# shutdownhook: +# class: org.quartz.plugins.management.ShutdownHookPlugin +# cleanShutdown: true + # 任务执行记录(官方提供,仅日志输出) +# triggHistory: +# class: org.quartz.plugins.history.LoggingTriggerHistoryPlugin + scheduler: + instanceId: AUTO + instanceName: DemoQuartz + threadPool: + threadCount: 20 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true diff --git a/job/ballcat-spring-boot-starter-quartz/src/test/resources/application.yml b/job/ballcat-spring-boot-starter-quartz/src/test/resources/application.yml new file mode 100644 index 00000000..9d04446a --- /dev/null +++ b/job/ballcat-spring-boot-starter-quartz/src/test/resources/application.yml @@ -0,0 +1,6 @@ +logging: + level: + root: info + org.ballcat: debug + org.quartz: debug + org.springframework.boot: debug diff --git a/pom.xml b/pom.xml index c95f4c82..39060ff8 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ ip/ballcat-spring-boot-starter-ip2region job/ballcat-spring-boot-starter-xxljob + job/ballcat-spring-boot-starter-quartz kafka/ballcat-kafka kafka/ballcat-kafka-stream