zl程序教程

您现在的位置是:首页 >  后端

当前栏目

springboot+quartz构建定时任务

SpringBoot 构建 任务 定时 quartz
2023-06-13 09:13:24 时间

springboot+quartz构建定时任务

开发环境

  • jdk:jdk1.8.0_212
  • maven:apache-maven-3.6.2
  • springboot版本:2.2.0

Quartz的3个基本要素

  • Scheduler:调度器。所有的调度都是由它控制。
  • Trigger: 触发器。决定什么时候来执行任务。
  • JobDetail & Job: JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。使用JobDetail + Job而不是Job,这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

如何使用

引入相关依赖

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

      <!--quartz-->
      <dependency>
          <groupId>org.quartz-scheduler</groupId>
          <artifactId>quartz</artifactId>
          <version>2.3.0</version>
      </dependency>

      <dependency>
          <groupId>org.quartz-scheduler</groupId>
          <artifactId>quartz-jobs</artifactId>
          <version>2.3.0</version>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context-support</artifactId>
          <version>4.3.13.RELEASE</version>
          <scope>compile</scope>
      </dependency>

      <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper</artifactId>
          <version>5.1.8</version>
      </dependency>

      <dependency>
          <groupId>cn.hutool</groupId>
          <artifactId>hutool-all</artifactId>
          <version>4.6.6</version>
      </dependency>

resource目录下创建quartz.properties

org.quartz.scheduler.instanceName = TestScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
org.quartz.jobStore.tablePrefix = QRTZ_
#org.quartz.jobStore.dataSource = 这项可以不填,我填了启动时候会报错org.quartz.JobPersistenceException: Failed to obtain DB connection from data source 'xxx': java.sql.SQLException: There is no DataSource named 'xxx'
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread= true
org.quartz.jobStore.misfireThreshold = 5000
org.quartz.jobStore.class= org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass= org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 在调度流程的第一步,也就是拉取待即将触发的triggers时,是上锁的状态,即不会同时存在多个线程拉取到相同的trigger的情况,也就避免的重复调度的危险。

quartz需要用到的表

可以去quartz官网下载对应版本的包 解压后再对应的dbTables目录下有各种数据库的建表语句

实体和其他相关类

实体:

/**
 * @author zjq
 * @date
 * <p>description:实体</p>
 */
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class JobAndTrigger {
    /**
     * 定时任务名称
     */
    private String jobName;
    /**
     * 定时任务组
     */
    private String jobGroup;
    /**
     * 定时任务全类名
     */
    private String jobClassName;
    /**
     * 触发器名称
     */
    private String triggerName;
    /**
     * 触发器组
     */
    private String triggerGroup;
    /**
     * 重复间隔
     */
    private BigInteger repeatInterval;
    /**
     * 触发次数
     */
    private BigInteger timesTriggered;
    /**
     * cron 表达式
     */
    private String cronExpression;
    /**
     * 时区
     */
    private String timeZoneId;
    /**
     * 定时任务状态
     */
    private String triggerState;
}

job工厂:

@Component
public class JobFactory extends AdaptableJobFactory {
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

     @Override
        protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
            //调用父类的方法
            Object jobInstance = super.createJobInstance(bundle);
            //进行注入
            capableBeanFactory.autowireBean(jobInstance);
            return jobInstance;
        }
}

调度器配置: 如果不配置这个启动时候会报错,无法注入org.quartz.Scheduler

@Configuration
public class SchedulerConfig implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private JobFactory jobFactory;
    @Autowired
    @Qualifier("dataSource")
    private DataSource primaryDataSource;

    @Override
     public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("任务已经启动..."+event.getSource());
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
       //获取配置属性
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        //在quartz.properties中的属性被读取并注入后再初始化对象
        propertiesFactoryBean.afterPropertiesSet();
        //创建SchedulerFactoryBean
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setQuartzProperties(propertiesFactoryBean.getObject());
        //使用数据源,自定义数据源
        factory.setDataSource(this.primaryDataSource);
        factory.setJobFactory(jobFactory);
        factory.setWaitForJobsToCompleteOnShutdown(true);//这样当spring关闭时,会等待所有已经启动的quartz job结束后spring才能完全shutdown。
        factory.setOverwriteExistingJobs(false);
        factory.setStartupDelay(1);
        return factory;
    }


    /*
     * 通过SchedulerFactoryBean获取Scheduler的实例
     */
    @Bean(name="scheduler")
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }


    @Bean
    public QuartzInitializerListener executorListener() {
       return new QuartzInitializerListener();
    }

调度器控制类

@Controller
@Slf4j
public class JobController {
    private final JobService jobService;

    @Autowired
    public JobController(JobService jobService) {
        this.jobService = jobService;
    }

    @GetMapping("/job")
    public String applicationMain() {
        return "/velocity/templates/job/list";
    }

    /**
     * 保存定时任务
     */
    @PostMapping(value = "/job/add")
    @ResponseBody
    public ResultModel<String> addJob(@RequestBody  JobDTO form) {
        try {
            jobService.addJob(form);
        } catch (Exception e) {
            return new ResultModel<>(false, LoggerUtil.convertThrowableToString(e));
        }

        return new ResultModel<String>(true, "新增成功");
    }

    /**
     * 删除定时任务
     */
    @DeleteMapping(value = "/job/delete")
    @ResponseBody
    public ResultModel<String> deleteJob(JobForm form) throws SchedulerException {
        if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
            return new ResultModel<>(false,"参数不能为空");
        }
        try{
            jobService.deleteJob(form);
        } catch (Exception e) {
            return new ResultModel<>(false, LoggerUtil.convertThrowableToString(e));
        }
        return new ResultModel<>(true, "删除成功");
    }

    /**
     * 暂停定时任务
     */
    @PutMapping(value = "/job/pause",params = "pause")
    @ResponseBody
    public ResultModel<String> pauseJob(JobForm form) throws SchedulerException {
        if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
            return new ResultModel<>(false,"参数不能为空");
        }
        try{
            jobService.pauseJob(form);
        } catch (Exception e) {
            return new ResultModel<>(false, LoggerUtil.convertThrowableToString(e));
        }
        return new ResultModel<>(true, "暂停成功");
    }

    /**
     * 恢复定时任务
     */
    @PutMapping(value = "/job/resume",params = "resume")
    @ResponseBody
    public ResultModel<String> resumeJob(JobForm form) throws SchedulerException {
        if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
            return new ResultModel<>(false,"参数不能为空");
        }

        try{
            jobService.resumeJob(form);
        } catch (Exception e) {
            return new ResultModel<>(false, LoggerUtil.convertThrowableToString(e));
        }
        return new ResultModel<>(true, "恢复成功");
    }

    /**
     * 修改定时任务,定时时间
     */
    @PutMapping(value = "/job/cron",params = "cron")
    @ResponseBody
    public ResultModel<String> cronJob(@Valid JobForm form) {
        try {
            jobService.cronJob(form);
        } catch (Exception e) {
            return new ResultModel<>(false, LoggerUtil.convertThrowableToString(e));
        }
        return new ResultModel<>(true, "修改成功");
    }

    @PostMapping(value = "/job/query")
    @ResponseBody
    public ResultModel<Page> jobList(@RequestBody JobDTO jobDTO) {
        Integer currentPage=1;
        Integer pageSize=10;
        if (ObjectUtil.isNull(currentPage)) {
            currentPage = 1;
        }
        if (ObjectUtil.isNull(pageSize)) {
            pageSize = 10;
        }
        //IPage<JobAndTrigger> all = null;
        Page<JobAndTrigger> all = null;
        try{
            all = jobService.list(currentPage, pageSize);
        } catch (Exception e) {
            return new ResultModel<>(false, null);
        }
        return new ResultModel<>(true, all);
    }

    @RequestMapping(value = "/job/editPrepare", method = RequestMethod.POST)
    public String editPrepare(Model model, @RequestBody JobDTO jobDTO) {
        model.addAttribute("requirementDTO", jobDTO);
        return "/velocity/templates/job/saveOrUpdate";
    }
```
调度器接口和实现类:
```java
/**
 * @author zjq
 * @date 2019
 * <p>description:定時任务接口</p>
 */
public interface JobService {
    /**
     * 添加并启动定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws Exception 异常
     */
    void addJob(JobDTO form) throws Exception;

    /**
     * 删除定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    void deleteJob(JobForm form) throws SchedulerException;

    /**
     * 暂停定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    void pauseJob(JobForm form) throws SchedulerException;

    /**
     * 恢复定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    void resumeJob(JobForm form) throws SchedulerException;

    /**
     * 重新配置定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws Exception 异常
     */
    void cronJob(JobForm form) throws Exception;

    /**
     * 查询定时任务列表
     *
     * @param currentPage 当前页
     * @param pageSize    每页条数
     * @return 定时任务列表
     */
    Page<JobAndTrigger> list(Integer currentPage, Integer pageSize);
}




@Service
@Slf4j
public class JobServiceImpl  implements JobService {
    private final Scheduler scheduler;
    private final JobMapper jobMapper;

    @Autowired
    public JobServiceImpl(Scheduler scheduler, JobMapper jobMapper) {
        this.scheduler = scheduler;
        this.jobMapper = jobMapper;
    }

    /**
     * 添加并启动定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @return {@link JobDetail}
     * @throws Exception 异常
     */
    @Override
    public void addJob(JobDTO form) throws Exception {
        // 启动调度器
        scheduler.start();

        // 构建Job信息
        JobDetail jobDetail = JobBuilder.newJob(JobUtil.getClass(form.getJobClassName()).getClass()).withIdentity(form.getJobClassName(), form.getJobGroup()).build();

        // Cron表达式调度构建器(即任务执行的时间)
        CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule(form.getCronExpression());

        //根据Cron表达式构建一个Trigger
        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(form.getJobClassName(), form.getJobGroup()).withSchedule(cron).build();

        try {
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            log.error("【定时任务】创建失败!", e);
            throw new Exception("【定时任务】创建失败!");
        }

    }

    /**
     * 删除定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    @Override
    public void deleteJob(JobForm form) throws SchedulerException {
        scheduler.pauseTrigger(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
        scheduler.unscheduleJob(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
        scheduler.deleteJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
    }

    /**
     * 暂停定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    @Override
    public void pauseJob(JobForm form) throws SchedulerException {
        scheduler.pauseJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
    }

    /**
     * 恢复定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws SchedulerException 异常
     */
    @Override
    public void resumeJob(JobForm form) throws SchedulerException {
        scheduler.resumeJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
    }

    /**
     * 重新配置定时任务
     *
     * @param form 表单参数 {@link JobForm}
     * @throws Exception 异常
     */
    @Override
    public void cronJob(JobForm form) throws Exception {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName());
            // 表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(form.getCronExpression());

            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);

            // 根据Cron表达式构建一个Trigger
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();

            // 按新的trigger重新设置job执行
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            log.error("【定时任务】更新失败!", e);
            throw new Exception("【定时任务】创建失败!");
        }
    }

    /**
     * 查询定时任务列表
     *
     * @param currentPage 当前页
     * @param pageSize    每页条数
     * @return 定时任务列表
     */
//    @Override
//    public IPage<JobAndTrigger> list(Integer currentPage, Integer pageSize) {
//
//        QueryWrapper queryWrapper = new QueryWrapper();
//        Page<JobAndTrigger> pojo = new Page<>(currentPage, pageSize);
//        IPage<JobAndTrigger> page = this.page(pojo, queryWrapper);
//        List<JobAndTrigger> list = JSON.parseArray(JSON.toJSONString(page.getRecords()), JobAndTrigger.class);
//        page.setRecords(list);
//        return page;
//    }

    @Override
    public Page<JobAndTrigger> list(Integer currentPage, Integer pageSize) {
        Page<JobAndTrigger> pojo = new Page<>(currentPage,pageSize);
        List<JobAndTrigger> list = jobMapper.list(pojo);
        pojo.setRecords(list);
        return pojo;
    }
    }
```

前端代码:
<div style="height:100%; overflow:scroll">
    <form class="layui-form" action="">
        <!--$csrfToken.hiddenField-->

        <!--场景查询条件-->
        <div class="layui-form-item">
            <label class="layui-form-label">任务名称</label>
            <div class="layui-input-inline">
                <input type="text" name="jobName" class="layui-input" id="jobName"/>
            </div>

            <label class="layui-form-label" style="width: 150px">任务状态:</label>
            <div class="layui-input-inline" style="width: 170px">
                <select name="jobType" id="jobType"  style="width: max-content">
                    <option value="" selected>请选择任务状态</option>
                    <option value="RUNNING">运行中</option>
                    <option value="PAUSED">暂停</option>
                </select>
            </div>

        </div>

        <div class="layui-form-item">
            <div class="layui-input-block">
                <button class="layui-btn" lay-submit lay-filter="job_query">查询</button>
                <button type="reset" class="layui-btn layui-btn-primary">重置</button>
            </div>
        </div>
    </form>
    <hr/>
    <table class="layui-table" id="job_list" lay-filter="job_list">
    </table>
</div>

<!-- 表头批量操作工具 -->
<script type="text/html" id="headToolbar">
    <div class="layui-btn-container">
        <button class="layui-btn layui-btn-sm" lay-event="add">新增</button>
    </div>
</script>

<!--表格中便捷toolbar-->
<script type="text/html" id="barTool">
    <a class="layui-btn layui-btn-xs" lay-size="lg" lay-event="job_edit" style="width: 50px">编辑</a>
    #*<a class="layui-btn layui-btn-xs" lay-size="lg" lay-event="job_remove">删除</a>*#
</script>

<script type="text/html" id="jobTypeBar">
    <div>
        {{# if(d.jobType === "MAIN"){ }}
        主干回归用例
        {{# }else if(d.jobType === "PROJECT"){ }}
        项目测试用例
        {{# }else{ }}
        自定义用例
        {{# } }}
    </div>
</script>

<script src="/js/3rdParty/jquery-3.2.1.min.js"></script>
<script src="/js/job/job_list.js"></script>   


<div>
    <form class="layui-form">
        <!--$csrfToken.hiddenField-->
        <br/>
        <br/>
        <!--projectId-->
        <input type="hidden" name="id" value="$!{jobDTO.id}">
##        <input type="hidden" id="tempDto" name="dto" value="$!{jobDTO}">
        <br/>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">任务名称:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="text" name="jobName" id="jobName" value="$!{jobDTO.jobName}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">任务组:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="text" name="jobGroup" id="jobGroup" value="$!{jobDTO.jobGroup}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">任务全类名:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="text" name="jobClassName" id="jobClassName" value="$!{jobDTO.jobClassName}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">cron表达式:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="text" name="cronExpression" id="cronExpression" value="$!{jobDTO.cronExpression}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">提醒信息:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="areatext" name="notice" id="notice" value="$!{jobDTO.notice}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>
        <div class="layui-form-item" style="width: 100%">
            <label class="layui-form-label" style="width: 20%">@人员数组:</label>
            <div class="layui-input-inline" style="width: 70%">
                <input type="areatext" name="atPersons" id="atPersons" value="$!{jobDTO.atPersons}" required
                       lay-verify="required"
                       class="layui-input projId"></input>
            </div>
        </div>

        <br/>

        <!--*************此处提交部分******************-->
        <div class="layui-form-item">
            <div class="layui-input-block">
                <button class="layui-btn layui-btn-sm" lay-submit lay-filter="onSubmit">保存</button>
                <button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>

            </div>
        </div>

    </form>

</div>