前言

基于之前做的前后端分离项目,整合quartz。前后端分离项目可以进我的主页查看。

任务调度在很多系统中都会用到,比如定时发送邮件短信,定时生成报表数据等等。如果是一些简单的定时任务,使用 @Schedule 注解就可以完成,但是当应用场景越来越复杂,@Schedule 已经不能满足需要,所以需要Quartz 上场,Quartz 目前是 Java 体系中最完善的定时方案。

在代码中实现了三个接口,分别是使用simpletrigger和crontrigger,然后基于crontrigger提供一个公共接口,所有前端都调用这个接口,按照格式传递数据给后台,后台实现公共的job方法就可以。


一、Quartz中API基本概念

  • Scheduler: Quartz 中的任务调度器,相当于一个容器,里边装着 jobdetail和trigger。容器中可以有多个jobdetail和trigger,但是各自的组及名称必须唯一,因为组及名称是 Scheduler 查找定位容器中某一对象的依据
  • Trigger触发器,表示什么时候去触发任务,有多个实现类,常用的是SimpleTrigger和CronTrigger。SimpleTrigger一般用于定义简单的触发规则,比如从2022年10月19日开始,到10月31日结束,每隔24小时触发一次任务,这样的规则一般使用SimpleTrigger就可以实现。如果是下边这种复杂的场景,比如 2022年10月19日开始,到12月31日结束,每个月最后一天触发一次任务,只能使用CronTrigger来实现
  • Job:表示一个任务接口,里边只有一个方法,表示执行逻辑
void execute(JobExecutionContext context) 
  • JobDetail任务执行实例,表示一个具体的可执行的调度程序,包括了任务的唯一标识和具体要执行的任务,

二、整合

1.导入包

新增quartz和fastjson包,fastjson用于json字符串和对象转换

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- druid依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!-- mybatisPlus 核心库 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.iherus.shiro</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.10</version>
        </dependency>

    </dependencies>

2.application.yml


spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/my?serverTimezone=GMT%2B8&useSSL=FALSE
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
  quartz:
    #数据库方式
    job-store-type: jdbc
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: DefaultQuartzScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            #自定义的数据源别名,与最下方一致
            dataSource: qzDB
            #与cron的调度有关,忽略重启期间的任务
            misfireThreshold: 1000
            #设置为true开启集群
            isClustered: false
            clusterCheckinInterval: 10000
            #如果开启,会没法给任务传参数
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
          dataSource:
            qzDB:
              #quartz默认使用c3p0连接池,修改成 Druid,需要自己提供链接类
              connectionProvider.class: com.han.myapp.config.quartz.DruidConnectionProvider
              maxConnection: 10
              driver: com.mysql.cj.jdbc.Driver
              URL: jdbc:mysql://127.0.0.1:3306/my?serverTimezone=GMT%2B8&useSSL=FALSE
              user: root
              password: root
  redis:
    port: 6379
    host: localhost
    database: 0
    keyPrefix: 'shiro:redis'
server:
  port: 9001
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.han.myapp.model
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
shiro:
  salt: 'han'
  hashIterations: 1024

3.QuartzConfig.java

package com.han.myapp.config.quartz;


import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import java.io.IOException;

@Configuration
public class QuartzConfig {

    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;

    @Bean
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean.getScheduler();
    }
}

4.DruidConnectionProvider 连接类

package com.han.myapp.config.quartz;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.quartz.SchedulerException;
import org.quartz.utils.ConnectionProvider;

import java.sql.Connection;
import java.sql.SQLException;

@Data
public class DruidConnectionProvider implements ConnectionProvider {
    //常量配置,需提供set方法,Quartz框架自动注入值。(这里用了Lombok的@Data,会自动生成getter和setter)
    /**
     * JDBC驱动
     */
    public String driver;
    /**
     * JDBC连接串,必须是URL
     */
    public String URL;
    /**
     * 数据库用户名,不是username
     */
    public String user;
    /**
     * 数据库用户密码
     */
    public String password;
    /**
     * 数据库最大连接数
     */
    public int maxConnection;
    /**
     * 数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。
     */
    public String validationQuery;
    private boolean validateOnCheckout;
    private int idleConnectionValidationSeconds;
    public String maxCachedStatementsPerConnection;
    private String discardIdleConnectionsSeconds;
    public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;
    public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;
    /**
     * Druid连接池
     */
    private DruidDataSource datasource;

    //接口实现

    @Override
    public Connection getConnection() throws SQLException {
        return datasource.getConnection();
    }

    @Override
    public void shutdown() {
        datasource.close();
    }

    @Override
    public void initialize() throws SQLException {
        if (this.URL == null) {
            throw new SQLException("DBPool could not be created: DB URL cannot be null");
        }
        if (this.driver == null) {
            throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");
        }
        if (this.maxConnection < 0) {
            throw new SQLException("DBPool maxConnections could not be created: Max connections must be greater than zero!");
        }
        datasource = new DruidDataSource();
        try {
            datasource.setDriverClassName(this.driver);
        } catch (Exception e) {
            try {
                throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);
            } catch (SchedulerException ignored) {
            }
        }
        datasource.setUrl(this.URL);
        datasource.setUsername(this.user);
        datasource.setPassword(this.password);
        datasource.setMaxActive(this.maxConnection);
        datasource.setMinIdle(1);
        datasource.setMaxWait(0);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(DEFAULT_DB_MAX_CONNECTIONS);
        if (this.validationQuery != null) {
            datasource.setValidationQuery(this.validationQuery);
            if (!this.validateOnCheckout) {
                datasource.setTestOnReturn(true);
            } else {
                datasource.setTestOnBorrow(true);
            }
            datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);
        }
    }
}

5.sql,基本sql在org.quartz.impl.jdbcjobstore包下

 另外我们需要一张额外的表记录任务调度信息,字段可以根据自己需要定义



-- ----------------------------
-- Table structure for `scheduler_data`
-- ----------------------------
DROP TABLE IF EXISTS `scheduler_data`;
CREATE TABLE `scheduler_data` (
  `id` varchar(40) NOT NULL,
  `taskName` varchar(100) NOT NULL,
  `cron` varchar(50) NOT NULL,
  `type` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


 
6.SchedulerUtilService 生成调度任务方法接口

package com.han.myapp.service;

import org.quartz.JobDataMap;

/**
 * @author han
 */
public interface SchedulerUtilService {
    /**
     * 添加定时任务Job
     * @param jobName
     * @param jobGroup
     * @param jobClassName
     * @param triggerName
     * @param triggerGroup
     * @param cronExpression
     */
    void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression);

    /**
     *
     * @param jobName
     * @param jobGroup
     * @param jobClassName
     * @param triggerName
     * @param triggerGroup
     * @param cronExpression
     * @param data 对象转为 jsonString
     */
    void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression,String data);

    /**
     *
     * @param jobName
     * @param jobGroup
     * @param jobClassName
     * @param triggerName
     * @param triggerGroup
     * @param cronExpression
     * @param jobDataMap
     */
    void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression, JobDataMap jobDataMap);



    /**
     * 暂停定时任务
     * @param jobName
     * @param jobGroup
     */
    void pauseSchedule(String jobName, String jobGroup);

    /**
     * 重启定时任务
     * @param jobName
     * @param jobGroup
     */
    void resumeSchedule(String jobName, String jobGroup);

    /**
     * 删除定时任务
     * @param jobName
     * @param jobGroup
     */
    void deleteSchedule(String jobName, String jobGroup);



}

生成调度任务实现类

package com.han.myapp.service.impl;

import com.han.myapp.service.SchedulerUtilService;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;


@Service
public class SchedulerUtilServiceImpl implements SchedulerUtilService {

    @Autowired
    private Scheduler scheduler;

    @Override
    public void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression) {
        try {
            Class className = Class.forName(jobClassName);
            QuartzJobBean jobBean = (QuartzJobBean) className.newInstance();
            JobDetail jobDetail = JobBuilder.newJob(jobBean.getClass())
                    .withIdentity(jobName, jobGroup)
                    .storeDurably()
                    .build();
            CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerName, triggerGroup)
                    .startNow()
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                    .build();
            scheduler.scheduleJob(jobDetail, cronTrigger);
            scheduler.start();
        } catch (SchedulerException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression, String data) {
        try {
            Class className = Class.forName(jobClassName);
            QuartzJobBean jobBean = (QuartzJobBean) className.newInstance();
            JobDetail jobDetail = JobBuilder.newJob(jobBean.getClass())
                    .withIdentity(jobName, jobGroup)
                    .usingJobData("data",data)
                    .storeDurably()
                    .build();
            CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerName, triggerGroup)
                    .startNow()
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                    .build();
            scheduler.scheduleJob(jobDetail, cronTrigger);
            scheduler.start();
        } catch (SchedulerException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void addSchedule(String jobName, String jobGroup, String jobClassName, String triggerName, String triggerGroup, String cronExpression, JobDataMap jobDataMap) {
        try {
            Class className = Class.forName(jobClassName);
            Job jobBean = (Job) className.newInstance();
            JobDetail jobDetail = JobBuilder.newJob(jobBean.getClass())
                    .withIdentity(jobName, jobGroup)
                    .usingJobData(jobDataMap)
                    .storeDurably()
                    .build();
            CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerName, triggerGroup)
                    .startNow()
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                    .build();
            scheduler.scheduleJob(jobDetail, cronTrigger);
            scheduler.start();
        } catch (SchedulerException | ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void pauseSchedule(String jobName, String jobGroup) {
        try {
            scheduler.pauseJob(JobKey.jobKey(jobName, jobGroup));
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void resumeSchedule(String jobName, String jobGroup) {
        try {
            scheduler.resumeJob(JobKey.jobKey(jobName, jobGroup));
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteSchedule(String jobName, String jobGroup) {
        try {
            scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup));
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

 

 7.任务调度信息

 SchedulerData.java

package com.han.myapp.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;


@Data
@EqualsAndHashCode(callSuper = false)
public class SchedulerData implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;

    @TableField("taskName")
    private String taskName;

    private String cron;

    private String type;


    public enum TYPE{

        EMAILJOB(1,"emailServiceImpl"),
        SMSJOB(2,"smsServiceImpl");

        public int code;

        public String className;


        TYPE(int code,String className){
            this.code = code;
            this.className = className;
        }

        public String getClassName(){return this.className;}


        public static TYPE get(Integer code) {
            if (code == null) {return null;}
            for (TYPE value : values()) {
                if (value.code == code) {
                    return value;
                }
            }
            return null;
        }

    }


}

mapper是逆向生成的,省略。。

vo

EmailVo 用于接收前台参数

package com.han.myapp.vo;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class EmailVo implements Serializable {

    private List<String> names;
    private String content;

    private String taskName;
    private String cron;

}

 

 job emailJob

package com.han.myapp.job;

import com.alibaba.fastjson.JSON;
import com.han.myapp.vo.EmailVo;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Slf4j
public class EmailJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm:ss");

        JobDataMap mergedJobDataMap = context.getMergedJobDataMap();
        String data = mergedJobDataMap.getString("data");
        EmailVo emailVo = JSON.parseObject(data, EmailVo.class);

        /*EmailVo emailVo = (EmailVo) mergedJobDataMap.get("data");*/


        List<String> names = emailVo.getNames();
        for (String name : names) {
            log.info(dateTimeFormatter.format(now)+"向"+name+"发送Email:"+emailVo.getContent());
        }

    }
}

 

 

生成调度任务 controller service impl

controller

package com.han.myapp.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.han.myapp.common.Result;
import com.han.myapp.model.SchedulerData;
import com.han.myapp.service.ISchedulerDataService;
import com.han.myapp.vo.EmailVo;
import com.han.myapp.vo.SchedulerDataVo;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;



@RestController
@RequestMapping("/scheduler/")
public class SchedulerController {

    @Autowired
    private ISchedulerDataService schedulerDataService;


    @GetMapping("jobList")
    public Result jobList(Page<SchedulerData> page,SchedulerData scheduler){
        LambdaQueryWrapper<SchedulerData> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StringUtils.hasLength(scheduler.getTaskName()),SchedulerData::getTaskName,scheduler.getTaskName());
        Page<SchedulerData> page1 = schedulerDataService.page(page, queryWrapper);
        return new Result().success().setData(page1);
    }

    /**
     * 测试 simpleTrigger
     * @return
     */
    @PostMapping("sendEmail")
    public Result sendEmail(@RequestBody EmailVo emailVo) throws SchedulerException {
        schedulerDataService.sendEmail(emailVo);
        return new Result().success();
    }

    @PostMapping("cronSendEmail")
    public Result cronSendEmail(@RequestBody EmailVo emailVo) {
        schedulerDataService.cronSendEmail(emailVo);
        return new Result().success();
    }


}

service

package com.han.myapp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.han.myapp.model.SchedulerData;
import com.han.myapp.vo.EmailVo;
import com.han.myapp.vo.SchedulerDataVo;
import org.quartz.SchedulerException;


public interface ISchedulerDataService extends IService<SchedulerData> {


    /**
     * 发送
     * @param emailVo
     */
    void sendEmail(EmailVo emailVo) throws SchedulerException;

    /**
     * cron 表达式发送
     * @param emailVo
     */
    void cronSendEmail(EmailVo emailVo);

   
}

 impl

package com.han.myapp.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.han.myapp.job.EmailJob;
import com.han.myapp.mapper.SchedulerDataMapper;
import com.han.myapp.model.SchedulerData;
import com.han.myapp.service.ISchedulerDataService;
import com.han.myapp.service.SchedulerUtilService;
import com.han.myapp.vo.EmailVo;
import com.han.myapp.vo.SchedulerDataVo;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Date;
import java.util.List;
import java.util.Map;


@Service
public class SchedulerDataServiceImpl extends ServiceImpl<SchedulerDataMapper, SchedulerData> implements ISchedulerDataService {

    @Autowired
    private Scheduler scheduler;

    @Autowired
    private SchedulerUtilService schedulerUtilService;




    @Override
    public void sendEmail(EmailVo emailVo) throws SchedulerException {

        Date date = new Date();
        date.setTime(1666656000000L);
        String s = JSON.toJSONString(emailVo);

        JobDetail jobDetail = JobBuilder.newJob(EmailJob.class).usingJobData("data", s).build();
        SimpleTrigger simpleTrigger =
                TriggerBuilder.newTrigger().
                        startNow().endAt(date).withSchedule(
                                SimpleScheduleBuilder.
                                        simpleSchedule().
                                        withIntervalInSeconds(10).
                                        withRepeatCount(10)).
                        withIdentity("name","group").build();
        scheduler.scheduleJob(jobDetail,simpleTrigger);
        scheduler.start();

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cronSendEmail(EmailVo emailVo) {

        SchedulerData schedulerData = new SchedulerData();
        schedulerData.setTaskName(emailVo.getTaskName());
        schedulerData.setCron(emailVo.getCron());
        schedulerData.setType("emailJob.class");
        save(schedulerData);

        JobDataMap jobDataMap = new JobDataMap();
        String s = JSON.toJSONString(emailVo);
        jobDataMap.put("data",s);
        //加入到定时任务
        schedulerUtilService.addSchedule(schedulerData.getId(),
                "email","com.han.myapp.job.EmailJob",
                schedulerData.getId(),"email", emailVo.getCron(),jobDataMap);
    }

 


}

8. 前台

router.config

 {
        path: '/timeTask',
        name: 'timeTask',
        component: RouteView,
        meta: {
          title: '定时任务',
          hidden: true,
          keepAlive: true,
          redirect:"/timeTask/manger",
        },
        children: [
          {
            path: '/timeTask/manger',
            name: 'timeTask-manger1',
            key:'timeTask-manger1',
            component: () => import('@/views/page/timeTask/TimeTaskList.vue'),
            meta: {
              title: '定时任务管理',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/simpleTrigger',
            name: 'timeTask-simpleTrigger',
            key:'timeTask-simpleTrigger',
            component: () => import('@/views/page/timeTask/SimpleTrigger.vue'),
            meta: {
              title: 'simpleTrigger',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/CronTrigger',
            name: 'timeTask-CronTrigger',
            key:'timeTask-CronTrigger',
            component: () => import('@/views/page/timeTask/CronTrigger.vue'),
            meta: {
              title: 'CronTrigger',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/pubCron',
            name: 'timeTask-pubCron',
            key:'timeTask-pubCron',
            component: () => import('@/views/page/timeTask/CronTrigger.vue'),
            meta: {
              title: 'CronTrigger',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/pubEmailCron',
            name: 'timeTask-pubEmailCron',
            key:'timeTask-pubEmailCron',
            component: () => import('@/views/page/timeTask/PubSchedulerEmail.vue'),
            meta: {
              title: 'pubEmailCron',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/pubSmsCron',
            name: 'timeTask-pubSmsCron',
            key:'timeTask-pubSmsCron',
            component: () => import('@/views/page/timeTask/PubSchedulerSms.vue'),
            meta: {
              title: 'pubSmsCron',
              hidden: false,
              keepAlive: false,
            }
          }
        ]
      }
TimeTaskList.vue
<template>
  <page-header-wrapper>
    <a-card :bordered="false">
      <div class="table-page-search-wrapper">
        <a-form layout="inline">
          <a-row :gutter="48">
            <a-col :md=" 4|| 24" :sm="24">
             <a-form-model-item label="任务名称">
               <a-input v-model="queryParam.taskName"/>
             </a-form-model-item>
            </a-col>

            <a-col :md=" 4|| 24" :sm="24">
              <span class="table-page-search-submitButtons" :style=" { float: 'right', overflow: 'hidden' } || {} ">
                <a-button type="primary"@click="$refs.table.refresh(true)">查询</a-button>
                <a-button style="margin-left: 8px" @click="() => this.queryParam = {}">重置</a-button>
              </span>
            </a-col>

          </a-row>
        </a-form>
      </div>

      <div class="table-operator">
        <a-button v-if="$auth('project:apply')" type="primary" @click="openDrawer('新增',null)">新增任务</a-button>
      </div>

      <s-table
        ref="table"
        size="default"
        rowKey="id"
        :columns="columns"
        :data="loadData"
        :alert="true"
        :rowSelection="rowSelection"
        showPagination="auto">
        <span slot="serial" slot-scope="text, record, index">
          {{ index + 1 }}
        </span>

        <span slot="action" slot-scope="text, record">
          <template>
            <a-space >
              <a-button type="primary" size="small">button</a-button>
            </a-space>



          </template>
        </span>
      </s-table>
    </a-card>
    <a-drawer
      :visible="drawer.visible"
      :title="drawer.title"
      placement="right"
      :width="500"
      :destroyOnClose="true"
      @close="drawer.visible=false"
    >
      <AddorEdit
        :record="record"
        :title="drawer.title"
        @closeDrawer="closeDrawer"
        @refrshTable="refrshTable"
      ></AddorEdit>
    </a-drawer>

  </page-header-wrapper>
</template>

<script>
import { STable, Ellipsis } from '@/components'
import AddorEdit from '@/views/page/timeTask/AddorEdit'
import {jobList} from "@/api/task";

const columns = [
  {
    title: '#',
    scopedSlots: { customRender: 'serial' }
  },
  {
    title: '任务名称',
    dataIndex: 'taskName'
  },
  {
    title: 'cron',
    dataIndex: 'cron'
  },
  {
    title: '类型',
    dataIndex: 'type',
    width: '150px',
  },
  {
    title: '操作',
    dataIndex: 'action',
    width: '150px',
    scopedSlots: { customRender: 'action' }
  },
]

export default {
  name: 'ProjectList',
  components: {
    AddorEdit,
    STable
  },

  data () {
    return {
      columns: columns,
      // create model
      visible: false,
      confirmLoading: false,
      // 查询参数
      queryParam: {},
      selectedRowKeys: [],
      selectedRows: [],
      addOrEdit: '新增',
      drawer: {
        visible: false,
        title: ''
      },
      record: {},
      projectData: [],
      queryType:'',
      // 加载数据方法 必须为 Promise 对象
      loadData: parameter => {
        const requestParameters = Object.assign({}, parameter, this.queryParam)
        return jobList(requestParameters).then(res => {
          console.log(res)
          return res
        })
      },
    }
  },
  created () {
  },
  computed: {
    rowSelection () {
      return {
        selectedRowKeys: this.selectedRowKeys,
        onChange: this.onSelectChange
      }
    },


  },
  methods: {
    openDrawer (title, row) {
      this.drawer.visible = true
      this.drawer.title = title
      this.record = row
    },
    closeDrawer () {
      this.drawer.visible = false
    },
    refrshTable () {
      this.$refs.table.refresh()
    },

    onSelectChange (selectedRowKeys, selectedRows) {
      this.selectedRowKeys = selectedRowKeys
      this.selectedRows = selectedRows
    },

    resetSearchForm () {
      this.queryParam = {
      }
    },

    
  },
}
</script>
simpleTrigger
<template>
  <page-header-wrapper>
    <a-card :body-style="{padding: '24px 32px'}" :bordered="false">
      <a-form-model ref="form" :model="triggerForm"  v-bind="formLayout">
        <a-form-model-item label="发送给" prop="names">
          <a-select
            mode="multiple"
            placeholder="Please select"

            style="width: 200px"
            @change="handleChange"
          >
            <a-select-option v-for="o in optionData" :key="o.value">
              {{o.value}}
            </a-select-option>
          </a-select>
        </a-form-model-item>
        <a-form-model-item label="发送内容" prop="triggerForm">
          <a-textarea v-model="triggerForm.content" :maxLenght="1500" :autoSize="{minRows:3,maxRows:7}" ></a-textarea>
        </a-form-model-item>

        <a-form-model-item :wrapper-col="{ span: 14, offset: 4 }">
          <a-button type="primary" @click="sendEmail">
            发送
          </a-button>
        </a-form-model-item>

      </a-form-model>


    </a-card>


  </page-header-wrapper>
</template>

<script>

import {sendEmail} from "@/api/task";

export default {
  name: 'SimpleTrigger',
  components: {

  },

  data () {
    return {
      triggerForm:{
        names:[],
        content:''
      },
      formLayout: {
        labelCol: { span: 4 },
        wrapperCol: {offset:1,span: 14 }
      },
      optionData:[
        {label:'张三',value:'张三'},
        {label:'李四',value:'李四'},
        {label:'王五',value:'王五'},
        {label:'钱六',value:'钱六'},
      ]
    }
  },
  created () {
  },
  computed: {


  },
  methods: {
    handleChange(value) {
      this.triggerForm.names = value
    },
    sendEmail(){
      sendEmail(this.triggerForm).then(res=>{
        this.$message.success("发送成功")
      })
    }
  },
  watch:{

  }
}
</script>

CronTrigger

<template>
  <page-header-wrapper>
    <a-card :body-style="{padding: '24px 32px'}" :bordered="false">
      <a-form-model ref="form" :model="form"  v-bind="formLayout">

        <a-form-model-item label="任务名称" prop="taskName">
          <a-input v-model="form.taskName"/>
        </a-form-model-item>

        <a-form-model-item label="生成规律" prop="cron">
          <!--       <a-cron ref="innerVueCron" v-decorator="['cronExpression', {'initialValue':'0 0 0 2 * ?',rules:
                    [{ required: true, message: '请输入cron表达式!' }]}]"  @change="setCorn"></a-cron>-->
          <j-cron ref="innerVueCron" v-model="form.cron"
          ></j-cron>
        </a-form-model-item>


      <a-form-model-item label="发送给" prop="names">
        <a-select
          mode="multiple"
          placeholder="Please select"

          style="width: 200px"
          @change="handleChange"
        >
          <a-select-option v-for="o in optionData" :key="o.value">
            {{o.value}}
          </a-select-option>
        </a-select>
      </a-form-model-item>
      <a-form-model-item label="发送内容" prop="triggerForm">
        <a-textarea v-model="form.content" :maxLenght="1500" :autoSize="{minRows:3,maxRows:7}" ></a-textarea>
      </a-form-model-item>
      </a-form-model>

      <footer-tool-bar :style="{width:'100%'}">
        <a-space>
          <a-button type="primary" @click="cronSendEmail()">保存</a-button>
        </a-space>
      </footer-tool-bar>
    </a-card>


  </page-header-wrapper>
</template>

<script>
import JCron from "@/views/Jcron/JCron";
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
import {cronSendEmail} from "@/api/task";

export default {
  name: 'CronTrigger',
  components: {
    JCron,
    FooterToolBar
  },

  data () {
    return {
      formLayout: {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 }
      },
      form:{
        taskName:'',
        cron:''
      },
      optionData:[
        {label:'张三',value:'张三'},
        {label:'李四',value:'李四'},
        {label:'王五',value:'王五'},
        {label:'钱六',value:'钱六'},
      ],


    }
  },
  created () {
  },
  computed: {


  },
  methods: {
    setCorn(data){
      if(data !== undefined){
        this.$nextTick(() => {
          this.form.cron=data
        })
      }
    },
    handleChange(value) {
      this.form.names = value
    },
    cronSendEmail(){
      cronSendEmail(this.form).then(res=>{
        this.$message.success("发送成功")
      })
    }
  },
  watch:{

  }
}
</script>

task.js

import request from '@/utils/request'


export function addJob(data) {
  return request({
    url: '/scheduler/addJob',
    method: 'post',
    data: data
  })
}

export function jobList(params) {
  return request({
    url: '/scheduler/jobList',
    method: 'get',
    params: params
  })
}


export function sendEmail(data) {
  return request({
    url: '/scheduler/sendEmail',
    method: 'post',
    data: data
  })
}

export function cronSendEmail(data) {
  return request({
    url: '/scheduler/cronSendEmail',
    method: 'post',
    data: data
  })
}


export function pubScheduler(data) {
  return request({
    url: '/scheduler/pubScheduler',
    method: 'post',
    data: data
  })
}

cron 效果

根据cron表达式 向这几个人发送消息,看一下后台控制台

9.cron公共接口方式

上边的这两种方式都比较简单,但是如果在系统中很多模块要用到任务调度的话,每个模块都要编写一遍 controller service,所以编写了一种通用的方式,具体代码如下

controller

 @PostMapping("pubScheduler")
    public Result pubScheduler(@RequestBody SchedulerDataVo schedulerDataVo) {
        schedulerDataService.pubScheduler(schedulerDataVo);
        return new Result().success();
    }

service

   /**
     *  公共 定时任务
     * @param schedulerDataVo
     */
    void pubScheduler(SchedulerDataVo schedulerDataVo);

impl

@Override
    public void pubScheduler(SchedulerDataVo schedulerDataVo) {

        //type 转换成 对应的class
        String type = schedulerDataVo.getType();
        String className = SchedulerData.TYPE.get(Integer.parseInt(type)).className;
        schedulerDataVo.setType(className);

        save(schedulerDataVo);
        Map<String, Object> data = schedulerDataVo.getData();
        JobDataMap jobDataMap = new JobDataMap();
        String s = JSON.toJSONString(data);
        jobDataMap.put("data",s);
        jobDataMap.put("className",className);
        //加入到定时任务
        schedulerUtilService.addSchedule(schedulerDataVo.getId(),
                "pubCronScheduler","com.han.myapp.job.SchedulerJob",
                schedulerDataVo.getId(),"email", schedulerDataVo.getCron(),jobDataMap);
    }

job SchedulerJob,job中没有具体执行逻辑,job中根据对应的类型,决定执行哪个接口实现类的方法

package com.han.myapp.job;

import com.han.myapp.service.PubScheduler;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Map;

@Slf4j
public class SchedulerJob implements Job {


    @Autowired
    private Map<String, PubScheduler> pubSchedulerMap;


    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();

        String className = mergedJobDataMap.getString("className");
        String data = mergedJobDataMap.getString("data");
        log.info(pubSchedulerMap.toString());

        PubScheduler pubScheduler = pubSchedulerMap.get(className);


        pubScheduler.doTask(data);


    }
}

 PubScheduler统一接口

package com.han.myapp.service;

/**
 * 任务 公共接口,没有具体逻辑,只决定调用哪个接口
 * 需要的地方直接实现该接口就可以
 */
public interface PubScheduler {

    void doTask(String data);
}

接口实现类,后续其他模块需要调度任务,需要实现该接口

测试代码

emailServiceImpl

package com.han.myapp.service.impl;

import com.alibaba.fastjson.JSON;
import com.han.myapp.job.SchedulerJob;
import com.han.myapp.service.PubScheduler;
import com.han.myapp.vo.EmailVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
@Slf4j
public class EmailServiceImpl implements PubScheduler {


    public void SendEmail(EmailVo emailVo){
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm:ss");


        List<String> names = emailVo.getNames();
        for (String name : names) {
            log.info(dateTimeFormatter.format(now)+"向"+name+"发送Email:"+emailVo.getContent());
        }
    }


    @Override
    public void doTask(String data) {

        EmailVo emailVo = JSON.parseObject(data, EmailVo.class);
        SendEmail(emailVo);

    }
}

 

SmsServiceImpl
package com.han.myapp.service.impl;

import com.alibaba.fastjson2.JSON;
import com.han.myapp.service.PubScheduler;
import com.han.myapp.vo.SmsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Slf4j
@Service
public class SmsServiceImpl implements PubScheduler {

    public void SendSms(SmsVo smsVo){
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd hh:mm:ss");


        List<String> phoneNumbers = smsVo.getPhoneNumber();
        for (String phoneNumber : phoneNumbers) {
            log.info(dateTimeFormatter.format(now)+"向"+phoneNumber+"发送短信:"+smsVo.getContent());
        }
        
        
    }


    @Override
    public void doTask(String data) {
        SmsVo smsVo = JSON.parseObject(data, SmsVo.class);

        SendSms(smsVo);

    }
}

 前台代码

router.config

{
            path: '/timeTask/pubEmailCron',
            name: 'timeTask-pubEmailCron',
            key:'timeTask-pubEmailCron',
            component: () => import('@/views/page/timeTask/PubSchedulerEmail.vue'),
            meta: {
              title: 'pubEmailCron',
              hidden: false,
              keepAlive: false,
            }
          },
          {
            path: '/timeTask/pubSmsCron',
            name: 'timeTask-pubSmsCron',
            key:'timeTask-pubSmsCron',
            component: () => import('@/views/page/timeTask/PubSchedulerSms.vue'),
            meta: {
              title: 'pubSmsCron',
              hidden: false,
              keepAlive: false,
            }
          }
PubSchedulerEmail
<template>
  <page-header-wrapper>
    <a-card :body-style="{padding: '24px 32px'}" :bordered="false">
      <a-form-model ref="form" :model="form"  v-bind="formLayout">

        <a-form-model-item label="任务名称" prop="taskName">
          <a-input v-model="form.taskName"/>
        </a-form-model-item>

        <a-form-model-item label="生成规律" prop="cron">
          <!--       <a-cron ref="innerVueCron" v-decorator="['cronExpression', {'initialValue':'0 0 0 2 * ?',rules:
                    [{ required: true, message: '请输入cron表达式!' }]}]"  @change="setCorn"></a-cron>-->
          <j-cron ref="innerVueCron" v-model="form.cron"
          ></j-cron>
        </a-form-model-item>


      <a-form-model-item label="发送给" prop="names">
        <a-select
          placeholder="Please select"
          mode="multiple"
          style="width: 200px"
          @change="handleChange"
        >
          <a-select-option value="张三">
            张三
          </a-select-option>
          <a-select-option value="李四">
            李四
          </a-select-option>
          <a-select-option value="王五">
            王五
          </a-select-option>
        </a-select>
      </a-form-model-item>
      <a-form-model-item label="发送内容" prop="triggerForm">
        <a-textarea v-model="form.content" :maxLenght="1500" :autoSize="{minRows:3,maxRows:7}" ></a-textarea>
      </a-form-model-item>
      </a-form-model>

      <footer-tool-bar :style="{width:'100%'}">
        <a-space>
          <a-button type="primary" @click="pubScheduler()">保存</a-button>
        </a-space>
      </footer-tool-bar>
    </a-card>


  </page-header-wrapper>
</template>

<script>
import JCron from "@/views/Jcron/JCron";
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
import {cronSendEmail, pubScheduler} from "@/api/task";

export default {
  name: 'CronTrigger',
  components: {
    JCron,
    FooterToolBar
  },

  data () {
    return {
      formLayout: {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 }
      },
      form:{
        taskName:'',
        cron:''
      },
      optionData:[
        {label:'张三',value:'张三'},
        {label:'李四',value:'李四'},
        {label:'王五',value:'王五'},
        {label:'钱六',value:'钱六'},
      ],


    }
  },
  created () {
  },
  computed: {


  },
  methods: {
    setCorn(data){
      if(data !== undefined){
        this.$nextTick(() => {
          this.form.cron=data
        })
      }
    },
    handleChange(value) {
      this.form.names = value
    },
    pubScheduler(){
      this.form.type=1

      this.form.data={
        names:this.form.names,
        content:this.form.content
      }

      pubScheduler(this.form).then(res=>{
        this.$message.success("发送成功")
      })
    }
  },
  watch:{

  }
}
</script>

 

PubSchedulerSms
<template>
  <page-header-wrapper>
    <a-card :body-style="{padding: '24px 32px'}" :bordered="false">
      <a-form-model ref="form" :model="form"  v-bind="formLayout">

        <a-form-model-item label="任务名称" prop="taskName">
          <a-input v-model="form.taskName"/>
        </a-form-model-item>

        <a-form-model-item label="生成规律" prop="cron">
          <!--       <a-cron ref="innerVueCron" v-decorator="['cronExpression', {'initialValue':'0 0 0 2 * ?',rules:
                    [{ required: true, message: '请输入cron表达式!' }]}]"  @change="setCorn"></a-cron>-->
          <j-cron ref="innerVueCron" v-model="form.cron"
          ></j-cron>
        </a-form-model-item>


      <a-form-model-item label="发送给" prop="phoneNumber">
        <a-select
          placeholder="Please select"
          mode="multiple"
          style="width: 200px"
          @change="handleChange"
        >
          <a-select-option value="1567777">
            1567777
          </a-select-option>
          <a-select-option value="15677799">
            15677799
          </a-select-option>
          <a-select-option value="1567779911">
            1567779911
          </a-select-option>
        </a-select>
      </a-form-model-item>
      <a-form-model-item label="发送内容" prop="triggerForm">
        <a-textarea v-model="form.content" :maxLenght="1500" :autoSize="{minRows:3,maxRows:7}" ></a-textarea>
      </a-form-model-item>
      </a-form-model>

      <footer-tool-bar :style="{width:'100%'}">
        <a-space>
          <a-button type="primary" @click="pubScheduler()">保存</a-button>
        </a-space>
      </footer-tool-bar>
    </a-card>


  </page-header-wrapper>
</template>

<script>
import JCron from "@/views/Jcron/JCron";
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
import { pubScheduler} from "@/api/task";

export default {
  name: 'CronTrigger',
  components: {
    JCron,
    FooterToolBar
  },

  data () {
    return {
      formLayout: {
        labelCol: { span: 4 },
        wrapperCol: { span: 14 }
      },
      form:{
        taskName:'',
        cron:'',
        phoneNumber:[]
      },



    }
  },
  created () {
  },
  computed: {


  },
  methods: {
    setCorn(data){
      if(data !== undefined){
        this.$nextTick(() => {
          this.form.cron=data
        })
      }
    },
    handleChange(value) {
      this.form.phoneNumber = value
    },
    pubScheduler(){
      this.form.type=2
      this.form.data={
        phoneNumber:this.form.phoneNumber,
        content:this.form.content
      }
      pubScheduler(this.form).then(res=>{
        this.$message.success("发送成功")
      })
    }
  },
  watch:{

  }
}
</script>

前台部分就是把 除了公共字段之外的字段 放到了 form.data中,后台使用map接收,这个map会先转换成jsonString,在接口实现类中转换成类中需要的类型,所以前台form.data中的字段要和最终转换的类字段一致

 


总结

以上就是我在前后端分离项目中整合quartz的过程,最后一种方法应该算是策略模式吧,不知道这样写有没有问题,如果有问题,还请大家多多指教

代码地址

前台gitee 地址

后台gitee地址

Logo

前往低代码交流专区

更多推荐