当前位置: 首页 > backend >正文

实现动态增QuartzJob,通过自定义注解调用相应方法

:::tip
动态增加Quartz定时任务,通过自定义注解来实现具体的定时任务方法调用。
:::
相关依赖如下

<!-- 用来动态创建 Quartz 定时任务 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

1. 注解及相关实体类

1. TaskDesc注解

用于描述定时任务的方法名和描述信息, 方便

import java.lang.annotation.*;/*** @author eleven* @date 2025/2/25 9:45* @apiNote*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TaskDesc {String methodName();String desc();
}

2. 任务实体类

@Data
@TableName("sys_task_config")
@ApiModel(value="定时任务配置")
public class TaskConfig extends BaseEntity {@ApiModelProperty("定时任务表达式")private String cron;@ApiModelProperty("执行类的全限定名")private String execClass;@ApiModelProperty("方法名")private String execMethod;@ApiModelProperty("是否运行")private Boolean startFlag;@ApiModelProperty("任务名称")private String cronName;public String getExecMethod() {return StrUtil.isNotBlank(execMethod) ? execMethod.replace("()", "").trim() : execMethod;}public String getExecClass() {return StrUtil.isNotBlank(execClass) ? execClass.trim(): execClass;}public String getCron() {return StrUtil.isNotBlank(cron) ? cron.trim() : cron;}
} 

3. 可选任务配置 vo

用于前端展示,前端配置定时任务的时候只能从 @TaskDesc 注解中获取到的方法名中选择。
也是为了限制前端用户乱填方法名,避免定时任务执行失败

@Data
public class TaskDescVo {private Integer index;private String beanName;private String className;private String methodName;private String desc;
} 

4. 任务执行记录

@Data
@TableName("credit_task_run_log")
@ApiModel("定时任务日志")
public class TaskRunLog extends BaseEntity<TaskRunLog> {@NotBlank(message = "任务id不能为空")private String taskId;@ApiModelProperty("任务开始时间")private LocalDateTime runTime;@ApiModelProperty("任务完成时间")private LocalDateTime completedTime;@ApiModelProperty("任务间隔时间")private Long intervalSeconds;@ApiModelProperty("任务运行状态")private Boolean runFlag;@ApiModelProperty("任务运行消息")private String message;public LocalDateTime getRunTime() {return getTime(runTime);}public LocalDateTime getCompletedTime() {return getTime(completedTime);}public LocalDateTime getTime(LocalDateTime time) {return Optional.ofNullable(time).orElse(LocalDateTime.now());}public Long getIntervalSeconds() {return Math.abs(Duration.between(getRunTime(), getCompletedTime()).getSeconds());}
}

5. CronDto

前端传入选择的执行时间,通过CronUtil生成cron表达式

import lombok.Data;import javax.validation.constraints.NotNull;
import java.util.List;/*** @author eleven* @date 2023/12/6 8:19* @apiNote*/
@Data
public class CronDto {/*** 选择的小时*/@NotNull(message = "执行小时参数不允许为空")private List<String> chooseHours;/*** 选择的天数*/private List<String> chooseDays;/*** 选择周几执行*/private List<String> chooseDayOfWeeks;
}

2. 定时任务配置

1. PostRunner

用于在项目启动的时候,从数据库中获取到所有的定时任务配置,然后根据配

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import net.lesscoding.task.domain.TaskConfig;
import net.lesscoding.task.service.TaskConfigService;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.util.List;/*** @author eleven* @date 2024/11/11 15:01* @apiNote*/
@Component
@Slf4j
public class PostRunner {@Autowiredprivate TaskConfigService taskConfigService;@Autowiredprivate SchedulerFactoryBean schedulerFactoryBean;@Autowiredprivate Gson gson;@PostConstructpublic void run() throws Exception {List<TaskConfig> planTaskList = taskConfigService.selectAll();log.info("==============定时任务配置中心日志开始====================");log.info("计划任务列表:{}", gson.toJson(planTaskList));log.info("==============定时任务配置中心日志结束====================");Scheduler scheduler = schedulerFactoryBean.getScheduler();if (CollUtil.isNotEmpty(planTaskList)) {for (TaskConfig planTask : planTaskList) {JobDetail jobDetail = JobBuilder.newJob(RunnerJob.class).withIdentity(planTask.getId(), StrUtil.format("{}#{}", planTask.getExecClass(), planTask.getExecMethod())).build();Trigger trigger = TriggerBuilder.newTrigger().withIdentity(planTask.getId(), StrUtil.format("{}#{}", planTask.getExecClass(), planTask.getExecMethod())).startNow().withSchedule(CronScheduleBuilder.cronSchedule(planTask.getCron())).build();scheduler.scheduleJob(jobDetail, trigger);scheduler.start();}}}
}

2. Job类

具体 Quartz 任务执行的 Job, Quartz 最终会调用 RunnerJobexecute 方法来执行定时任务

import net.lesscoding.task.service.TaskConfigService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;/*** @author eleven* @date 2024/11/11 14:22* @apiNote*/
@Slf4j
@Component
public class RunnerJob implements Job {@Autowiredprivate TaskConfigService taskConfigService;@Overridepublic void execute(JobExecutionContext jobExecutionContext) {JobDetail jobDetail = jobExecutionContext.getJobDetail();JobKey key = jobDetail.getKey();String planId = key.getName();log.info("{} trigger {}", planId, ((CronTriggerImpl) jobExecutionContext.getTrigger()).getCronExpression());log.info("{} jobKey {} time {}", planId, key, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));log.info("定时任务开始执行");try {taskConfigService.runPlan(jobDetail);} catch (Exception e) {throw new RuntimeException(e);}}
}

3. 定时任务控制器

用于前端展示定时任务配置,以及新增、修改、删除定时任务配置

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import net.lesscoding.task.core.annotations.Log;
import net.lesscoding.task.core.common.AjaxResult;
import net.lesscoding.task.core.enums.BusinessType;
import net.lesscoding.task.domain.CronDto;
import net.lesscoding.task.domain.TaskConfig;
import net.lesscoding.task.service.TaskConfigService;
import net.lesscoding.task.utils.ResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;/*** @author eleven* @date 2024/11/11 15:56* @apiNote*/@Api(tags = "定时任务配置")
@RestController
@RequestMapping("/task/config")
public class TaskConfigController {@Autowiredprivate TaskConfigService taskConfigService;@ApiOperation("查询配置列表")@PostMapping("/page")public AjaxResult page(@RequestBody TaskConfig taskConfig) {Page<TaskConfig> list = taskConfigService.getConfigList(taskConfig);return ResultUtil.success(list);}@ApiOperation("编辑配置")@PostMapping("/edit")@Log(title = "编辑定时任务配置", businessType = BusinessType.UPDATE)public AjaxResult edit(@RequestBody TaskConfig taskConfig) throws SchedulerException {return ResultUtil.toAjax(taskConfigService.editTaskConfig(taskConfig));}@PostMapping("/getCron")@ApiOperation("获取表达式")public AjaxResult getCron(@Valid @RequestBody CronDto dto) {return ResultUtil.success(taskConfigService.getCron(dto));}@ApiOperation("删除配置")@DeleteMapping("/del/{id}")@Log(title = "删除定时任务配置", businessType = BusinessType.DELETE)public AjaxResult del(@PathVariable String id) throws SchedulerException {return ResultUtil.toAjax(taskConfigService.delTaskConfig(id));}@ApiOperation("获取所有任务列表")@GetMapping("/taskList")public AjaxResult taskList() {return ResultUtil.success(taskConfigService.getAllTaskDescList());}
}

4. ServiceImpl实现类

用于实现定时任务的具体逻辑,包括获取所有任务列表、获取表达式、编辑配置、删除配置、获取配置列表、运行计划等方法

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import net.lesscoding.task.core.annotations.TaskDesc;
import net.lesscoding.task.core.common.GlobalException;
import net.lesscoding.task.dao.TaskConfigMapper;
import net.lesscoding.task.dao.TaskRunLogMapper;
import net.lesscoding.task.domain.CronDto;
import net.lesscoding.task.domain.TaskConfig;
import net.lesscoding.task.domain.TaskRunLog;
import net.lesscoding.task.model.vo.TaskDescVo;
import net.lesscoding.task.runner.RunnerJob;
import net.lesscoding.task.service.TaskConfigService;
import net.lesscoding.task.utils.CronUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;/*** @author eleven* @date 2024/11/11 14:21* @apiNote*/
@Service
@Slf4j
public class TaskConfigServiceImpl extends ServiceImpl<TaskConfigMapper, TaskConfig> implements TaskConfigService {@Autowiredprivate TaskConfigMapper configMapper;@Autowiredprivate TaskRunLogMapper runLogMapper;@Autowiredprivate ConfigurableListableBeanFactory beanFactory;@Autowiredprivate SchedulerFactoryBean schedulerFactoryBean;@Autowiredprivate ApplicationContext applicationContext;@Overridepublic List<TaskConfig> selectAll() {return configMapper.selectList(new QueryWrapper<>());}/*** 具体执行任务的方法* @param jobDetail Quartz的JobDetail对象,包含任务的详细信息* @return*/@Override@Asyncpublic void runPlan(JobDetail jobDetail) {JobKey key = jobDetail.getKey();String taskId = key.getName();TaskRunLog runLog = new TaskRunLog();runLog.setId(IdUtil.simpleUUID());runLog.setTaskId(taskId);runLog.setRunTime(LocalDateTime.now());TaskConfig taskConfig = configMapper.selectById(taskId);if (taskConfig == null || !taskConfig.getStartFlag()) {String logStr = StrUtil.format("任务ID {} 不存在或配置为关闭 {}", taskId, taskConfig);log.info(logStr);runLog.setRunFlag(false);runLog.setCompletedTime(LocalDateTime.now());runLog.setMessage(logStr);runLogMapper.insert(runLog);return;}String className = taskConfig.getExecClass();String methodName = taskConfig.getExecMethod();try {// 这里可以直接通过 applicationContext 获取到类的实例// Object bean = applicationContext.getBean(className);// 加载类并获取实例Class<?> execClass = getClass().getClassLoader().loadClass(className);// 从Spring容器中获取实例Object bean = beanFactory.getBean(execClass);// 获取方法Method execMethod = execClass.getDeclaredMethod(methodName);// 执行方法Object invoke = execMethod.invoke(bean);runLog.setRunFlag(true);runLog.setMessage(String.valueOf(invoke));} catch (Exception e) {runLog.setRunFlag(false);runLog.setMessage(e.getCause().getMessage());log.error("执行任务失败", e);}runLog.setCompletedTime(LocalDateTime.now());runLogMapper.insert(runLog);}@Overridepublic Page<TaskConfig> getConfigList(TaskConfig taskConfig) {PageDTO page = taskConfig.getPage();List<TaskConfig> list = configMapper.getPageByLike(page, taskConfig);page.setRecords(list);return page;}@Overridepublic int editTaskConfig(TaskConfig taskConfig) throws SchedulerException {checkEditTaskConfig(taskConfig);if (StrUtil.isBlank(taskConfig.getId())) {return saveTaskConfig(taskConfig);}return updateTaskConfig(taskConfig);}@Overridepublic int delTaskConfig(String id) throws SchedulerException {TaskConfig taskConfig = configMapper.selectById(id);deleteJob(taskConfig);return configMapper.deleteById(id);}private void checkEditTaskConfig(TaskConfig taskConfig) {boolean valid = CronUtil.isValid(taskConfig.getCron());if (!valid) {throw new GlobalException("cron表达式不合法");}try {Class<?> execClass = getClass().getClassLoader().loadClass(taskConfig.getExecClass());Object bean = beanFactory.getBean(execClass);if (bean == null) {throw new GlobalException("请检查当前类名是否存在");}Method declaredMethod = execClass.getDeclaredMethod(taskConfig.getExecMethod());if (declaredMethod == null) {throw new GlobalException(StrUtil.format("请检查当前方法{}#{}()是否存在", taskConfig.getExecClass(), taskConfig.getExecMethod()));}} catch (ClassNotFoundException e) {throw new GlobalException("请检查当前类名是否存在");} catch (NoSuchMethodException e) {throw new GlobalException(StrUtil.format("请检查当前方法{}#{}()是否存在", taskConfig.getExecClass(), taskConfig.getExecMethod()));}List<TaskConfig> allTasks = selectAll();List<TaskConfig> sameTaskList = allTasks.stream().filter(item -> StrUtil.equals(item.getExecClass(), taskConfig.getExecClass())&& StrUtil.equals(item.getExecMethod(), taskConfig.getExecMethod())).collect(Collectors.toList());if (CollUtil.isNotEmpty(sameTaskList)) {// 新增任务的时候存在相同的类名和方法名if (StrUtil.isBlank(taskConfig.getId())) {throw new GlobalException(StrUtil.format("任务{}.{}()已存在", taskConfig.getExecClass(), taskConfig.getExecMethod()));}// 修改任务的时候存在相同的类名和方法名if (sameTaskList.size() == 1 && !StrUtil.equals(sameTaskList.get(0).getId(), taskConfig.getId())) {throw new GlobalException(StrUtil.format("任务{}.{}()已存在", taskConfig.getExecClass(), taskConfig.getExecMethod()));}}}public int saveTaskConfig(TaskConfig taskConfig) throws SchedulerException {taskConfig.setId(IdUtil.simpleUUID());int effect = configMapper.insert(taskConfig);createNewScheduler(taskConfig);return effect;}public int updateTaskConfig(TaskConfig taskConfig) throws SchedulerException {deleteJob(configMapper.selectById(taskConfig.getId()));int effect = configMapper.updateById(taskConfig);createNewScheduler(taskConfig);return effect;}private void createNewScheduler(TaskConfig task) throws SchedulerException {log.info("开始执行创建新任务");Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = jobKey(task);JobDetail jobDetail = JobBuilder.newJob(RunnerJob.class).withIdentity(jobKey).build();Trigger trigger = TriggerBuilder.newTrigger().withIdentity(task.getId(), StrUtil.format("{}#{}", task.getExecClass(), task.getExecMethod())).startNow().withSchedule(CronScheduleBuilder.cronSchedule(task.getCron())).build();scheduler.scheduleJob(jobDetail, trigger);scheduler.start();log.info("任务创建完成");}/*** 阐述job** @param task* @throws SchedulerException*/public boolean deleteJob(TaskConfig task) throws SchedulerException {Scheduler scheduler = schedulerFactoryBean.getScheduler();JobKey jobKey = jobKey(task);boolean deleteJob = scheduler.deleteJob(jobKey);log.info("当前 jobKey {} 删除结果{}", jobKey, deleteJob);return deleteJob;}private JobKey jobKey(TaskConfig task) {JobKey jobKey = new JobKey(task.getId(), StrUtil.format("{}#{}", task.getExecClass(), task.getExecMethod()));log.info("当前任务 {}, jobKey{}", task, jobKey);return jobKey;}@Overridepublic String getCron(CronDto dto) {boolean daysEmptyFlag = CollUtil.isEmpty(dto.getChooseDays());boolean dayOfWeeksEmptyFlag = CollUtil.isEmpty(dto.getChooseDayOfWeeks());if (daysEmptyFlag && dayOfWeeksEmptyFlag) {throw new RuntimeException("执行天数和星期必须选择一个");}if (!daysEmptyFlag && !dayOfWeeksEmptyFlag) {throw new RuntimeException("执行天数和星期只能选择一个");}String hours = String.join(",", dto.getChooseHours());String days = CollUtil.isEmpty(dto.getChooseDays()) ? "?" : String.join(",", dto.getChooseDays());String dayOfWeek = CollUtil.isEmpty(dto.getChooseDayOfWeeks()) ? "?" : String.join(",", dto.getChooseDayOfWeeks());String cronStr = String.format("0 0 %s %s * %s", hours, days, dayOfWeek);if (!CronUtil.isValid(cronStr)) {throw new RuntimeException("定时任务表达式不合法");}log.info("当前任务表达式 {}", cronStr);return cronStr;}@Overridepublic List<TaskDescVo> getAllTaskDescList() {List<TaskDescVo> result = new ArrayList<>();List<String> beanNames = new ArrayList<>(Arrays.asList(applicationContext.getBeanDefinitionNames()));beanNames.sort(String::compareTo);TaskDescVo vo = null;for (String beanName : beanNames) {Object bean = applicationContext.getBean(beanName);// 使用 AopUtils 来获取代理对象的原始类, 否则获得的是代理类,无法获取@Service等类上的注解Class<?> beanClass = AopUtils.getTargetClass(bean);if (beanClass.isAnnotationPresent(TaskDesc.class)) {TaskDesc annotation = beanClass.getAnnotation(TaskDesc.class);vo = new TaskDescVo();vo.setMethodName(annotation.methodName());vo.setDesc(annotation.desc());vo.setBeanName(beanName);vo.setClassName(beanClass.getName());vo.setIndex(beanNames.indexOf(beanName));result.add(vo);}}return result;}private CronDto parseCron(String cron) {String[] split = cron.split(" ");// 计算几个小时String cronHours = split[2];// 计算几天String cronDays = split[3];// 计算的周期String cronDayOfWeeks = split[5];CronDto cronDto = new CronDto();cronDto.setChooseHours(Arrays.asList(cronHours.split(",")));cronDto.setChooseDays(Arrays.asList(cronDays.split(",")));cronDto.setChooseDayOfWeeks(Arrays.asList(cronDayOfWeeks.split(",")));return cronDto;}
}

5. CronUtil

import org.quartz.CronExpression;import java.text.ParseException;
import java.util.Date;/*** cron表达式工具类** @author ruoyi*/
public class CronUtil {/*** 返回一个布尔值代表一个给定的Cron表达式的有效性** @param cronExpression Cron表达式* @return boolean 表达式是否有效*/public static boolean isValid(String cronExpression) {return CronExpression.isValidExpression(cronExpression);}public static void main(String[] args) {System.out.println(isValid("0/1 * * * * ?"));}/*** 返回一个字符串值,表示该消息无效Cron表达式给出有效性** @param cronExpression Cron表达式* @return String 无效时返回表达式错误描述,如果有效返回null*/public static String getInvalidMessage(String cronExpression) {try {new CronExpression(cronExpression);return null;} catch (ParseException pe) {return pe.getMessage();}}/*** 返回下一个执行时间根据给定的Cron表达式** @param cronExpression Cron表达式* @return Date 下次Cron表达式执行时间*/public static Date getNextExecution(String cronExpression) {try {CronExpression cron = new CronExpression(cronExpression);return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));} catch (ParseException e) {throw new IllegalArgumentException(e.getMessage());}}
}

6. @TaskDesc注解使用

@TaskDesc注解的类需要使用@Component注解标注,被SpringBoot容器管理到定时任务才能正常执行

import net.lesscoding.task.core.annotations.TaskDesc;
import net.lesscoding.task.dao.EvaluateDsCustomerMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;/*** @author eleven* @date 2025/3/12 15:11* @apiNote 客户评分计数器*/
@Component
@Slf4j
@TaskDesc(methodName = "scoreCounter", desc = "客户评分计数器")
public class CustomerScoreCounter extends AbstractScoreCounter {@Autowiredprivate EvaluateDsCustomerMapper dsCustomerMapper;@Autowiredprivate JexlEngine jexlEngine;// 定时任务实际执行的方法@Overridepublic void scoreCounter() {calcScoreAndSave(2, null, "customer_id",dsCustomerMapper.selectList(null));}
}
http://www.xdnf.cn/news/7810.html

相关文章:

  • OpenCV CUDA模块特征检测与描述------一种基于快速特征点检测和旋转不变的二进制描述符类cv::cuda::ORB
  • WPF核心类继承树结构
  • 学习路之uniapp--unipush2.0推送功能--服务端推送消息
  • Java安全-Servlet内存马
  • 基于多传感器融合的智能驾驶环境感知系统
  • 【java第19集】java面向对象编程详解
  • MyBatis:简化数据库操作的持久层框架
  • 高噪声下扩展边缘检测算子对检测边缘的影响
  • windows powershell 判断 进程号是否存在
  • 无人机桥梁巡检
  • linux文件重命名命令
  • MIL-C-5015航空插头2芯震动加速度传感器连接器
  • 五、【API 开发篇(下)】:使用 Django REST Framework构建测试用例模型的 CRUD API
  • 云原生安全之PaaS:从基础到实践的技术指南
  • 谈谈 Kotlin 中的构造方法,有哪些注意事项?
  • 【Django系统】Python+Django携程酒店评论情感分析系统
  • 【Java微服务组件】异步通信P2—Kafka与消息
  • [杂学笔记]浏览器多进程与多线程架构、wstring类型、哈希表、红黑树与哈希表的对比、C++标准库Random类
  • 影响镍钯金PCB表面处理价格的因素有哪些?
  • Spring事务简单操作
  • 【低代码】如何使用明道云调用 Flask 视图函数并传参(POST 方法实践)
  • vue-cli 构建打包优化(JeecgBoot-Vue2 配置优化篇)
  • Hadoop-HA高可用集群启动nameNode莫名挂掉,排错解决
  • digitalworld.local: FALL靶场
  • Mysql-数据闪回工具MyFlash
  • SQL查询, 响应体临时字段报: Unknown column ‘data_json_map‘ in ‘field list‘
  • leetcode 92. Reverse Linked List II
  • 张 Prompt Tuning--中文数据准确率提升:理性与冲动识别新突破
  • 分类算法 Kmeans、KNN、Meanshift 实战
  • maven之pom.xml