mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-17 04:25:06 +08:00
项目结构调整 x 16 : 将 monitor、sms、dict 等组件拆分出去
This commit is contained in:
@ -0,0 +1,18 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.quartz.core.scheduler.SchedulerManager;
|
||||
import org.quartz.Scheduler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling // 开启 Spring 自带的定时任务
|
||||
public class QuartzConfig {
|
||||
|
||||
@Bean
|
||||
public SchedulerManager schedulerManager(Scheduler scheduler) {
|
||||
return new SchedulerManager(scheduler);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.enums;
|
||||
|
||||
/**
|
||||
* Quartz Job Data 的 key 枚举
|
||||
*/
|
||||
public enum JobDataKeyEnum {
|
||||
|
||||
JOB_ID,
|
||||
JOB_HANDLER_NAME,
|
||||
JOB_HANDLER_PARAM,
|
||||
JOB_RETRY_COUNT, // 最大重试次数
|
||||
JOB_RETRY_INTERVAL, // 每次重试间隔
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.handler;
|
||||
|
||||
/**
|
||||
* 任务处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface JobHandler {
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @param param 参数
|
||||
* @return 结果
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
String execute(String param) throws Exception;
|
||||
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.handler;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.service.JobLogFrameworkService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.PersistJobDataAfterExecution;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.scheduling.quartz.QuartzJobBean;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.diff;
|
||||
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
|
||||
|
||||
/**
|
||||
* 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@DisallowConcurrentExecution
|
||||
@PersistJobDataAfterExecution
|
||||
@Slf4j
|
||||
public class JobHandlerInvoker extends QuartzJobBean {
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Resource
|
||||
private JobLogFrameworkService jobLogFrameworkService;
|
||||
|
||||
@Override
|
||||
protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException {
|
||||
// 第一步,获得 Job 数据
|
||||
Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name());
|
||||
String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name());
|
||||
String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name());
|
||||
int refireCount = executionContext.getRefireCount();
|
||||
int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0);
|
||||
int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0);
|
||||
|
||||
// 第二步,执行任务
|
||||
Long jobLogId = null;
|
||||
Date startTime = new Date();
|
||||
String data = null;
|
||||
Throwable exception = null;
|
||||
try {
|
||||
// 记录 Job 日志(初始)
|
||||
jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1);
|
||||
// 执行任务
|
||||
data = this.executeInternal(jobHandlerName, jobHandlerParam);
|
||||
} catch (Throwable ex) {
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
// 第三步,记录执行日志
|
||||
this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext);
|
||||
|
||||
// 第四步,处理有异常的情况
|
||||
handleException(exception, refireCount, retryCount, retryInterval);
|
||||
}
|
||||
|
||||
private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception {
|
||||
// 获得 JobHandler 对象
|
||||
JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class);
|
||||
Assert.notNull(jobHandler, "JobHandler 不会为空");
|
||||
// 执行任务
|
||||
return jobHandler.execute(jobHandlerParam);
|
||||
}
|
||||
|
||||
private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception,
|
||||
JobExecutionContext executionContext) {
|
||||
Date endTime = new Date();
|
||||
// 处理是否成功
|
||||
boolean success = exception == null;
|
||||
if (!success) {
|
||||
data = getRootCauseMessage(exception);
|
||||
}
|
||||
// 更新日志
|
||||
try {
|
||||
jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data);
|
||||
} catch (Exception ex) {
|
||||
log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]",
|
||||
executionContext.getJobDetail().getKey(), jobLogId, success, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleException(Throwable exception,
|
||||
int refireCount, int retryCount, int retryInterval) throws JobExecutionException {
|
||||
// 如果有异常,则进行重试
|
||||
if (exception == null) {
|
||||
return;
|
||||
}
|
||||
// 情况一:如果到达重试上限,则直接抛出异常即可
|
||||
if (refireCount >= retryCount) {
|
||||
throw new JobExecutionException(exception);
|
||||
}
|
||||
|
||||
// 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试
|
||||
// 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。
|
||||
if (retryInterval > 0) {
|
||||
ThreadUtil.sleep(retryInterval);
|
||||
}
|
||||
// 第二个参数,refireImmediately = true,表示立即重试
|
||||
throw new JobExecutionException(exception, true);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.scheduler;
|
||||
|
||||
import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker;
|
||||
import org.quartz.*;
|
||||
|
||||
/**
|
||||
* {@link org.quartz.Scheduler} 的管理器,负责创建任务
|
||||
*
|
||||
* 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即:
|
||||
* 1. Job 的 {@link JobDetail#getKey()}
|
||||
* 2. Trigger 的 {@link Trigger#getKey()}
|
||||
*
|
||||
* 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SchedulerManager {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
public SchedulerManager(Scheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Job 到 Quartz 中
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param retryCount 重试次数
|
||||
* @param retryInterval 重试间隔
|
||||
* @throws SchedulerException 添加异常
|
||||
*/
|
||||
public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval)
|
||||
throws SchedulerException {
|
||||
// 创建 JobDetail 对象
|
||||
JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class)
|
||||
.usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId)
|
||||
.usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName)
|
||||
.withIdentity(jobHandlerName).build();
|
||||
// 创建 Trigger 对象
|
||||
Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
|
||||
// 新增调度
|
||||
scheduler.scheduleJob(jobDetail, trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Job 到 Quartz
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param retryCount 重试次数
|
||||
* @param retryInterval 重试间隔
|
||||
* @throws SchedulerException 更新异常
|
||||
*/
|
||||
public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval)
|
||||
throws SchedulerException {
|
||||
// 创建新 Trigger 对象
|
||||
Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
|
||||
// 修改调度
|
||||
scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 删除异常
|
||||
*/
|
||||
public void deleteJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.deleteJob(new JobKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 暂停异常
|
||||
*/
|
||||
public void pauseJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.pauseJob(new JobKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 启动异常
|
||||
*/
|
||||
public void resumeJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.resumeJob(new JobKey(jobHandlerName));
|
||||
scheduler.resumeTrigger(new TriggerKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即触发一次 Quartz 中的 Job
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @throws SchedulerException 触发异常
|
||||
*/
|
||||
public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam)
|
||||
throws SchedulerException {
|
||||
JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval
|
||||
data.put(JobDataKeyEnum.JOB_ID.name(), jobId);
|
||||
data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName);
|
||||
data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam);
|
||||
// 触发任务
|
||||
scheduler.triggerJob(new JobKey(jobHandlerName), data);
|
||||
}
|
||||
|
||||
private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval) {
|
||||
return TriggerBuilder.newTrigger()
|
||||
.withIdentity(jobHandlerName)
|
||||
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
|
||||
.usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam)
|
||||
.usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount)
|
||||
.usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.service;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Job 日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface JobLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 Job 日志
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param beginTime 开始时间
|
||||
* @param jobHandlerName Job 处理器的名字
|
||||
* @param jobHandlerParam Job 处理器的参数
|
||||
* @param executeIndex 第几次执行
|
||||
* @return Job 日志的编号
|
||||
*/
|
||||
Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId,
|
||||
@NotNull(message = "开始时间") Date beginTime,
|
||||
@NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName,
|
||||
String jobHandlerParam,
|
||||
@NotNull(message = "第几次执行不能为空") Integer executeIndex);
|
||||
|
||||
/**
|
||||
* 更新 Job 日志的执行结果
|
||||
*
|
||||
* @param logId 日志编号
|
||||
* @param endTime 结束时间。因为是异步,避免记录时间不准去
|
||||
* @param duration 运行时长,单位:毫秒
|
||||
* @param success 是否成功
|
||||
* @param result 成功数据
|
||||
*/
|
||||
void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId,
|
||||
@NotNull(message = "结束时间不能为空") Date endTime,
|
||||
@NotNull(message = "运行时长不能为空") Integer duration,
|
||||
boolean success, String result);
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.util;
|
||||
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quartz Cron 表达式的工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CronUtils {
|
||||
|
||||
/**
|
||||
* 校验 CRON 表达式是否有效
|
||||
*
|
||||
* @param cronExpression CRON 表达式
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValid(String cronExpression) {
|
||||
return CronExpression.isValidExpression(cronExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 CRON 表达式,获得下 n 个满足执行的时间
|
||||
*
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param n 数量
|
||||
* @return 满足条件的执行时间
|
||||
*/
|
||||
public static List<Date> getNextTimes(String cronExpression, int n) {
|
||||
// 获得 CronExpression 对象
|
||||
CronExpression cron;
|
||||
try {
|
||||
cron = new CronExpression(cronExpression);
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
// 从当前开始计算,n 个满足条件的
|
||||
Date now = new Date();
|
||||
List<Date> nextTimes = new ArrayList<>(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Date nextTime = cron.getNextValidTimeAfter(now);
|
||||
nextTimes.add(nextTime);
|
||||
// 切换现在,为下一个触发时间;
|
||||
now = nextTime;
|
||||
}
|
||||
return nextTimes;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 定时任务,采用 Quartz 实现进程内的任务执行。
|
||||
* 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.quartz;
|
Reference in New Issue
Block a user