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

实现动态增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));}
}

相关文章:

  • Sentinel原理与SpringBoot整合实战
  • spring-retry
  • 【springcloud核心技术站概述】
  • Dynamics 365 Business Central Azure application registration
  • Docker 镜像打包到本地
  • ArcGIS Pro 3.4 二次开发 - 核心主机
  • 《大模型开源与闭源的深度博弈:科技新生态下的权衡与抉择》
  • 中天智能装备科技有限公司:智能仓储领域的卓越之选​
  • Android OkHttp控制链:深入理解网络请求的流程管理
  • SpringBoot3+Vue3(2)-前端基本页面配置-登录界面编写-Axios请求封装-后端跨越请求错误
  • Android 自定义SnackBar和下滑取消
  • java基础 之 Hash家族(一)
  • 和风天气 API 获取天气预报 2025/5/21
  • Axure通过下拉框选项改变,控制字段显隐藏
  • 多技术栈 iOS 项目的性能调试实战:从 Flutter 到 Unity(含 KeyMob 工具实测)
  • 【Qt】QImage::Format
  • Kotlin 极简小抄 P8(不可空类型、可空类型、注意事项、非空断言 !!)
  • linux文件重命名命令
  • html,js获取扫码设备的输入内容
  • HTTPS和HTTP区别
  • 腾讯布局智能体开发平台,吴运声:智能体行业增长很快,仍处于百花齐放阶段
  • 上海古镇“长效”发展需要提高社会资本参与
  • 董明珠孟羽童官宣,5月23日将合体直播
  • 中国需加强自主创新和国际合作,提升产业链供应链韧性
  • 可显著提高公交出行率,山东、浙江多县常态化实施城区公交免费
  • 黄仁勋:新一代计算平台GB300三季度上市,AI计算能力每十年提升100万倍