eladmin——定时任务

一、前言

实现定时任务的方式有很多,本文重点介绍Quartz在eladmin中的应用。

二、Quartz简介

Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。

2.1 核心概念

  • Job(任务): Job 是 Quartz 中用于执行具体任务逻辑的接口或抽象类,开发者需要实现该接口或继承该类,并编写实际的任务逻辑。每个 Job 都必须定义一个执行的任务。
java 复制代码
public interface Job {
    void execute(JobExecutionContext var1) throws JobExecutionException;
}
  • JobDetail(任务详情): JobDetail 是对 Job 的具体实例化和描述,它定义了 Job 的属性和配置信息,包括 Job 的名称、所属的组、Job 类型、Job 是否持久化等。JobDetail 用来描述和配置 Job,作为调度器执行的任务对象。

  • Trigger(触发器):- Trigger 用于定义任务的触发条件,即何时执行任务。Quartz 提供了多种类型的触发器,包括简单触发器(SimpleTrigger)和 Cron 触发器(CronTrigger)。触发器可以设置任务的执行时间、执行频率、重复次数等。

  • Scheduler(调度器):Scheduler 是 Quartz 的核心组件,负责触发和执行 JobScheduler 可以启动、暂停、恢复和停止任务调度。它管理着 JobTrigger,根据 Trigger 的设定来触发 Job 的执行。

  • JobStore(任务存储):JobStore 是 Quartz 中用于存储 JobTrigger 的接口,它定义了如何存储和管理调度的任务。Quartz 提供了多种实现 JobStore 的方式,包括内存存储、数据库存储等。

  • Listener(监听器):Quartz 提供了一套监听器机制,可以监听调度器、触发器和任务的各种事件。开发者可以通过监听器来处理任务执行前后的逻辑,例如记录日志、发送通知等。

三、 Quartz在eladmin中的应用

3.1 定义Job

eladmin 源码

java 复制代码
@Async
public class ExecutionJob extends QuartzJobBean {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    // 此处仅供参考,可根据任务执行情况自定义线程池参数
    private final ThreadPoolTaskExecutor executor = SpringContextHolder.getBean("elAsync");

    @Override
    public void executeInternal(JobExecutionContext context) {
        // 获取任务
        QuartzJob quartzJob = (QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY);
        // 获取spring bean
        QuartzLogRepository quartzLogRepository = SpringContextHolder.getBean(QuartzLogRepository.class);
        QuartzJobService quartzJobService = SpringContextHolder.getBean(QuartzJobService.class);
        RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);

        String uuid = quartzJob.getUuid();

        QuartzLog log = new QuartzLog();
        log.setJobName(quartzJob.getJobName());
        log.setBeanName(quartzJob.getBeanName());
        log.setMethodName(quartzJob.getMethodName());
        log.setParams(quartzJob.getParams());
        long startTime = System.currentTimeMillis();
        log.setCronExpression(quartzJob.getCronExpression());
        try {
            // 执行任务
            QuartzRunnable task = new QuartzRunnable(quartzJob.getBeanName(), quartzJob.getMethodName(), quartzJob.getParams());
            Future<?> future = executor.submit(task);
            future.get();
            long times = System.currentTimeMillis() - startTime;
            log.setTime(times);
            if(StringUtils.isNotBlank(uuid)) {
                redisUtils.set(uuid, true);
            }
            // 任务状态
            log.setIsSuccess(true);
            logger.info("任务执行成功,任务名称:" + quartzJob.getJobName() + ", 执行时间:" + times + "毫秒");
            // 判断是否存在子任务
            if(StringUtils.isNotBlank(quartzJob.getSubTask())){
                String[] tasks = quartzJob.getSubTask().split("[,,]");
                // 执行子任务
                quartzJobService.executionSubJob(tasks);
            }
        } catch (Exception e) {
            if(StringUtils.isNotBlank(uuid)) {
                redisUtils.set(uuid, false);
            }
            logger.error("任务执行失败,任务名称:" + quartzJob.getJobName());
            long times = System.currentTimeMillis() - startTime;
            log.setTime(times);
            // 任务状态 0:成功 1:失败
            log.setIsSuccess(false);
            log.setExceptionDetail(ThrowableUtil.getStackTrace(e));
            // 任务如果失败了则暂停
            if(quartzJob.getPauseAfterFailure() != null && quartzJob.getPauseAfterFailure()){
                quartzJob.setIsPause(false);
                //更新状态
                quartzJobService.updateIsPause(quartzJob);
            }
            if(quartzJob.getEmail() != null){
                EmailService emailService = SpringContextHolder.getBean(EmailService.class);
                // 邮箱报警
                if(StringUtils.isNoneBlank(quartzJob.getEmail())){
                    EmailVo emailVo = taskAlarm(quartzJob, ThrowableUtil.getStackTrace(e));
                    emailService.send(emailVo, emailService.find());
                }
            }
        } finally {
            quartzLogRepository.save(log);
        }
    }

    private EmailVo taskAlarm(QuartzJob quartzJob, String msg) {
        EmailVo emailVo = new EmailVo();
        emailVo.setSubject("定时任务【"+ quartzJob.getJobName() +"】执行失败,请尽快处理!");
        Map<String, Object> data = new HashMap<>(16);
        data.put("task", quartzJob);
        data.put("msg", msg);
        TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH));
        Template template = engine.getTemplate("taskAlarm.ftl");
        emailVo.setContent(template.render(data));
        List<String> emails = Arrays.asList(quartzJob.getEmail().split("[,,]"));
        emailVo.setTos(emails);
        return emailVo;
    }
}

解读:

  • QuartzJobBean介绍:QuartzJobBean帮助简化Quartz Job的实现,并提供与Spring框架的无缝集成。QuartzJobBean提供了一个executeInternal方法,该方法会在每次触发Job时被调用,以执行具体的任务逻辑。开发人员只需专注于编写业务逻辑代码,而不必关心与Quartz框架的集成细节。
  • 业务逻辑

3.2 自定义线程池elAsync及定义QuartzRunnable

细心的同学会发现在QuartzJobBean中使用到了自定义线程池elAsync和来执行定时任务,为何要这样做呢?我们先看源码,首先是自定义线程池:

scss 复制代码
@Configuration
public class CustomExecutorConfig {

    /**
     * 自定义线程池,用法 @Async
     * @return Executor
     */
    @Bean
    @Primary
    public Executor elAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(AsyncTaskProperties.corePoolSize);
        executor.setMaxPoolSize(AsyncTaskProperties.maxPoolSize);
        executor.setQueueCapacity(AsyncTaskProperties.queueCapacity);
        executor.setThreadNamePrefix("el-async-");
        executor.setKeepAliveSeconds(AsyncTaskProperties.keepAliveSeconds);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 自定义线程池,用法 @Async("otherAsync")
     * @return Executor
     */
    @Bean
    public Executor otherAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(15);
        executor.setQueueCapacity(50);
        executor.setKeepAliveSeconds(AsyncTaskProperties.keepAliveSeconds);
        executor.setThreadNamePrefix("el-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

然后是QuartzRunnable源码:

kotlin 复制代码
@Slf4j
public class QuartzRunnable implements Callable<Object> {

   private final Object target;
   private final Method method;
   private final String params;

   QuartzRunnable(String beanName, String methodName, String params)
         throws NoSuchMethodException, SecurityException {
      this.target = SpringContextHolder.getBean(beanName);
      this.params = params;
      if (StringUtils.isNotBlank(params)) {
         this.method = target.getClass().getDeclaredMethod(methodName, String.class);
      } else {
         this.method = target.getClass().getDeclaredMethod(methodName);
      }
   }

   @Override
   @SuppressWarnings("all")
   public Object call() throws Exception {
      ReflectionUtils.makeAccessible(method);
      if (StringUtils.isNotBlank(params)) {
         method.invoke(target, params);
      } else {
         method.invoke(target);
      }
      return null;
   }
}

解读:

  • 为何要使用线程池:
    • 看到线程池,就要想起线程池的作用------------提高线程的利用率、减少资源开销(避免线程反复创建销毁)进而提高性能、控制并发度等;而自定义线程池 elAsync 则在此基础上增加了灵活性、扩展性和定制化的优势。Quartz结合线程池可以并发处理多个定时任务。
  • QuartzRunnable的作用:封装一个要在 Quartz 调度器中执行的任务,并提供了灵活的方式来调用目标方法
    • 首先在实例化QuartzRunnable对象时,要传入beanName、methodName、params三个属性,用于定位要执行的目标方法,再利用Java的反射机制调用目标方法达到执行定时任务的目的。

3.3 定义QuartzManage

QuartzManage中通过JobDetailTrigger以及Scheduler三者组合,实现添加定时任务,删除定时任务,暂停定时任务等操作。

scss 复制代码
@Slf4j
@Component
public class QuartzManage {

    private static final String JOB_NAME = "TASK_";

    @Resource
    private Scheduler scheduler;
    
    /**
     * 
     * @param quartzJob
     */
    public void addJob(QuartzJob quartzJob){
        try {
            // 构建job信息
            JobDetail jobDetail = JobBuilder.newJob(ExecutionJob.class).
                    withIdentity(JOB_NAME + quartzJob.getId()).build();

            //通过触发器名和cron 表达式创建 Trigger
            Trigger cronTrigger = newTrigger()
                    .withIdentity(JOB_NAME + quartzJob.getId())
                    .startNow()
                    .withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression()))
                    .build();

            cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob);

            //重置启动时间
            ((CronTriggerImpl)cronTrigger).setStartTime(new Date());

            //执行定时任务
            scheduler.scheduleJob(jobDetail,cronTrigger);

            // 暂停任务
            if (quartzJob.getIsPause()) {
                pauseJob(quartzJob);
            }
        } catch (Exception e){
            log.error("创建定时任务失败", e);
            throw new BadRequestException("创建定时任务失败");
        }
    }

    /**
     * 更新job cron表达式
     * @param quartzJob /
     */
    public void updateJobCron(QuartzJob quartzJob){
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            // 如果不存在则创建一个定时任务
            if(trigger == null){
                addJob(quartzJob);
                trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            }
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression());
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
            //重置启动时间
            ((CronTriggerImpl)trigger).setStartTime(new Date());
            trigger.getJobDataMap().put(QuartzJob.JOB_KEY,quartzJob);

            scheduler.rescheduleJob(triggerKey, trigger);
            // 暂停任务
            if (quartzJob.getIsPause()) {
                pauseJob(quartzJob);
            }
        } catch (Exception e){
            log.error("更新定时任务失败", e);
            throw new BadRequestException("更新定时任务失败");
        }

    }

    /**
     * 删除一个job
     * @param quartzJob /
     */
    public void deleteJob(QuartzJob quartzJob){
        try {
            JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
            scheduler.pauseJob(jobKey);
            scheduler.deleteJob(jobKey);
        } catch (Exception e){
            log.error("删除定时任务失败", e);
            throw new BadRequestException("删除定时任务失败");
        }
    }

    /**
     * 恢复一个job
     * @param quartzJob /
     */
    public void resumeJob(QuartzJob quartzJob){
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            // 如果不存在则创建一个定时任务
            if(trigger == null) {
                addJob(quartzJob);
            }
            JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
            scheduler.resumeJob(jobKey);
        } catch (Exception e){
            log.error("恢复定时任务失败", e);
            throw new BadRequestException("恢复定时任务失败");
        }
    }

    /**
     * 立即执行job
     * @param quartzJob /
     */
    public void runJobNow(QuartzJob quartzJob){
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            // 如果不存在则创建一个定时任务
            if(trigger == null) {
                addJob(quartzJob);
            }
            JobDataMap dataMap = new JobDataMap();
            dataMap.put(QuartzJob.JOB_KEY, quartzJob);
            JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
            scheduler.triggerJob(jobKey,dataMap);
        } catch (Exception e){
            log.error("定时任务执行失败", e);
            throw new BadRequestException("定时任务执行失败");
        }
    }

    /**
     * 暂停一个job
     * @param quartzJob /
     */
    public void pauseJob(QuartzJob quartzJob){
        try {
            JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId());
            scheduler.pauseJob(jobKey);
        } catch (Exception e){
            log.error("定时任务暂停失败", e);
            throw new BadRequestException("定时任务暂停失败");
        }
    }
}

解读:

这里面的代码逻辑比较简单,看代码注释也能理解,就不一一画流程图了,后面有机会补上。

相关推荐
2401_854391084 分钟前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端
虽千万人 吾往矣25 分钟前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
这孩子叫逆1 小时前
Spring Boot项目的创建与使用
java·spring boot·后端
coderWangbuer2 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上2 小时前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志2 小时前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba3 小时前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
千里码aicood4 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍4 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455664 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端