Java创建定时任务的几种方式,你还不会?

1,809 阅读9分钟

这是我参与8月更文挑战的第3天

前言

Hi,大家好,我是希留。 在项目的开发工程中,经常有需要定时任务调度的功能场景,比如,定时发送短信、定时抽取数据等。那遇到这种功能的时候应该怎么去做呢? 本篇文章向大家介绍几种创建定时任务的方式。如果对你有帮助的话,还不忘点赞支持一下,感谢!文末附有源码

一、单机环境下创建

1.使用TimerTask创建定时任务

Timer是jdk中提供的一个定时器类,通过该类可以为指定的定时任务进行配置。TimerTask类是一个定时任务类,该类实现了Runnable接口, 缺点是Timer的内部只有一个线程,如果有多个任务的话就会顺序执行,这样我们的延迟时间和循环时间就会出现问题。

public class JobTimerTask {
    static long count = 0;
    public static void main(String[] args) {      
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                count++;
                System.out.println(count);
            }
        };
        //创建timer对象设置间隔时间
        Timer timer = new Timer();
        // 间隔天数
        long delay = 0;
        // 间隔毫秒数
        long period = 1000;
        timer.scheduleAtFixedRate(timerTask, delay, period);
    }

2.使用线程池创建定时任务

ScheduledExecutorService线程池:相对延迟或者周期作为定时任务调度,缺点是没有绝对的日期或者时间​。

public class JobScheduledExecutorTask {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello ScheduledExecutorService定时任务!!");
            }
        };
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        // 第二个参数为首次执行的延时时间,第三个参数为定时执行的间隔时间
        service.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS);
    }
}

3.使用spring内置定时任务@Scheduled 注解

@Schedule注解:配置简单功能较多,如果系统使用单机的话可以优先考虑spring定时器。

@Component
@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class ScheduleTask {

    @Scheduled(cron = "0/5 * * * * ?")  //3.添加定时任务
    //@Scheduled(fixedRate=5000)        //或直接指定时间间隔,例如:5秒
    private void configureTasks() {
        System.out.println("执行静态定时任务时间: " + LocalDateTime.now());
    }
  }

二、分布式环境下创建

1.使用Quartz框架

Quartz是一个开源的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能。

Quartz 的常见集群方案是通过在数据库中配置定时器信息, 以数据库悲观锁的方式达到同一个任务始终只有一个节点在运行。亮点是保证节点高可用 (HA), 如果某一个节点挂了, 其他节点可以顶上。

缺点是同一个任务只能有一个节点运行,其他节点将不执行任务,性能低,资源浪费当碰到大量短任务时,各个节点频繁的竞争数据库锁,节点越多这种情况越严重。性能会很低下。

quartz 的分布式仅解决了集群高可用的问题,并没有解决任务分片的问题,不能实现水平扩展。

下面带大家介绍一下两种整合Quartz框架实现定时任务调度的方式,一种是Spring整合的方式,比较繁琐,但是能更好的掌握原理。一种是SpringBoot整合的方式,使用SpirngBoot自带Quartz插件,可以快速集成。

1.1 Spring集成方式

1.1.1 添加pom依赖

<!-- spring集成quartz定时任务-->
<dependency>
	<groupId>org.quartz-scheduler</groupId>
	<artifactId>quartz</artifactId>
</dependency>
<!-- spring为了整合quartz及其他框架的中间环境包-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-context-support</artifactId>
	<version>5.2.3.RELEASE</version>
</dependency>

1.1.2 初始化Quartz的表

如果需要做持久化的话,数据肯定是要存在数据库的,Quartz提供了相关的表结构,可以直接到官网下。

1.1.3 初始化自定义的定时任务表

在自定义一张定时任务表,把所有的定时任务都集中起来,方便进行业务操作管理。

-- 定时任务表结构
DROP TABLE IF EXISTS `schedule_job`;
CREATE TABLE `schedule_job`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `bean` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'bean名称',
  `method` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '方法名',
  `params` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '参数',
  `cron` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'cron表达式',
  `status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态。0:运行中;1:已暂停;2:已完成;3:运行失败;',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '定时任务表' ROW_FORMAT = Dynamic;

1.1.4 编写配置类

@Configuration
public class SchedulerConfig {

    @Autowired
    private TaskSchedulerFactory springJobFactory;

    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
        // 创建Scheduler的工厂
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setJobFactory(springJobFactory);

        // quartz参数
        factory.setQuartzProperties(this.convertProp());
        factory.setSchedulerName("JavaSjzlScheduler");
        // 延时启动 应用启动30秒后
        factory.setStartupDelay(30);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        // 可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        // 设置自动启动,默认为true
        factory.setAutoStartup(true);

        return factory;
    }

    private Properties convertProp() {
        // quartz参数,也可以写在配置文件里
        Properties prop = new Properties();

        // 实例名字 #默认或是自己改名字都行
        prop.put("org.quartz.scheduler.instanceName", "JavaSjzlScheduler");
        // #如果使用集群,instanceId必须唯一,设置成AUTO
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        // 线程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        // 并发个数
        prop.put("org.quartz.threadPool.threadCount", "10");
        // # 设置工作者线程的优先级(最大值10,最小值1,常用值5)
        prop.put("org.quartz.threadPool.threadPriority", "5");
        // JobStore配置 #存储方式使用JobStoreTX,也就是数据库
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        // 集群配置
        prop.put("org.quartz.jobStore.isClustered", "true");
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");

        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        // #数据库中quartz表的表名前缀
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");

        return prop;
    }
}

1.1.5 编写自定义的工厂类

由于定时任务Job对象的实例化过程是在Quartz中进行的,需要把将Job Bean也纳入到Spring容器的管理之中,以便在Spring容器启动后,Scheduler自动开始工作,而在Spring容器关闭前,自动关闭Scheduler。

/**
 * @author Java升级之路
 * @description 自定义的工厂类  创建job实例并将其纳入到Spring容器的管理之中
 * @date 2021/7/1
 */
@Component
public class TaskSchedulerFactory extends AdaptableJobFactory {

    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    protected Object creatJobInstance(TriggerFiredBundle bundle) throws Exception {
        //首先,调用父类的方法创建好quartz所需要的实例
        Object jobInstance = super.createJobInstance(bundle);
        //然后,使用BeanFactory为创建好的Job实例进行属性自动装配,并将其纳入到Spring容器的管理之中
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

1.1.6 编写一个测试任务调度类

**
 * @author Java升级之路
 * @description 测试任务调度类
 * @date 2021/7/1
 */
@Slf4j
@Component
public class HelloJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 获得传入的参数
        Object params = jobExecutionContext.getMergedJobDataMap().get(JobConstant.JOB_MAP_KEY);
        log.info("helloJob is running params={}, time:{}", params, new Date());
    }
}

1.1.7 编写一个任务调度工具类

把创建定时任务封装成一个工具类,方便业务上调用


/**
 * @author Java升级之路
 * @description 执行任务工具类
 * @date 2021/7/1
 */
public class ScheduleUtil {

    private final static String JOB_KEY = "JOB_";
    /**
     * 获取schedulerBean
     **/
    private final static Scheduler scheduler = SpringContextUtil.getBean(Scheduler.class);

    /**
     * 创建定时任务
     **/
    public static void create(Long jobId, Job job) {
        try {
            //构建job信息
            JobDetail jobDetail = JobBuilder.newJob(ScheduleUtil.getJobClass(job.getBean()).getClass())
                    .withIdentity(getJobKey(jobId))
                    .build();
            //表达式调度构建器(即任务执行的时间)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCron());
            //按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId))
                    .withSchedule(scheduleBuilder).build();
            // 传入参数
            jobDetail.getJobDataMap().put(JobConstant.JOB_MAP_KEY, job.getParams());
            scheduler.scheduleJob(jobDetail, trigger);

            // 默认创建时任务设置为暂停
            ScheduleUtil.pause(jobId);
        } catch (SchedulerException e) {
            throw new RuntimeException("创建定时任务失败");
        }
    }
    /**
     * 暂停定时任务
     **/
    public static void pause(Long jobId) {
        try {
            scheduler.pauseJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new RuntimeException("暂停定时任务失败");
        }
    }
    /**
     * 恢复启动定时任务
     **/
    public static void resume(Long jobId) {
        try {
            scheduler.resumeJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new RuntimeException("恢复启动定时任务失败");
        }
    }
    /**
     * 更新启动定时任务
     **/
    public static void update(Long jobId, Job job) {
        try {
            TriggerKey triggerKey = getTriggerKey(jobId);
            // 表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCron());
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            // 按新的cronExpression表达式重新构建trigger
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
            // 更新参数
            trigger.getJobDataMap().put(JobConstant.JOB_MAP_KEY, job.getParams());
            // 按新的trigger重新设置job执行
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            throw new RuntimeException("更新定时任务失败");
        }
    }
    /**
     * 删除定时任务
     **/
    public static void delete(Long jobId) {
        try {
            scheduler.pauseTrigger(getTriggerKey(jobId));
            scheduler.unscheduleJob(getTriggerKey(jobId));
            scheduler.deleteJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new RuntimeException("删除定时任务失败");
        }
    }

    public static QuartzJobBean getJobClass(String classname) {
        return (QuartzJobBean) SpringContextUtil.getBean(classname);
    }
    public static JobKey getJobKey(Long jobId) {
        return JobKey.jobKey(JOB_KEY + jobId);
    }

    public static TriggerKey getTriggerKey(Long jobId) {
        return TriggerKey.triggerKey(JOB_KEY + jobId);
    }
}

1.1.8 编写controller类

编写controller类,提供增删改的相关接口


@Slf4j
@RestController
@RequestMapping(value = "schedule/job")
public class JobController {

    @Autowired
    private JobService jobService;


    /**
     * 添加定时任务
     * @param job 任务
     */
    @PostMapping(value = "add")
    public R add(Job job) {
        jobService.createJob(job);
        return R.ok(null);
    }

    /**
     * 暂停定时任务
     * @param id 任务ID
     */
    @PostMapping(value = "pause/{id}")
    public R pause(@PathVariable Long id) {
        jobService.pauseJob(id);
        return R.ok(null);
    }

    /**
     * 恢复定时任务
     * @param id 任务ID
     */
    @PostMapping(value = "resume/{id}")
    public R resume(@PathVariable Long id) {
        jobService.resumeJob(id);
        return R.ok(null);
    }

    /**
     * 更新定时任务
     * @param job 任务
     */
    @PostMapping(value = "update")
    public R update(Job job) {
        jobService.updateJob(job);
        return R.ok(null);
    }

    /**
     * 删除定时任务
     * @param id 任务ID
     */
    @PostMapping(value = "delete/{id}")
    public R delete(@PathVariable Long id) {
        jobService.deleteJob(id);
        return R.ok(null);
    }
}

1.1.9 编写service类

@Service
public class JobServiceImpl extends ServiceImpl<JobMapper, Job> implements JobService {


    @Transactional(rollbackFor = Exception.class)
    @Override
    public void createJob(Job job) {
        job.setStatus(JobConstant.JOB_STATUS_PAUSE);
        super.save(job);
        ScheduleUtil.create(job.getId(), job);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void pauseJob(Long id) {
        Job job = this.checkJobExist(id);
        job.setStatus(JobConstant.JOB_STATUS_PAUSE);
        super.updateById(job);
        ScheduleUtil.pause(id);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void resumeJob(Long id) {
        Job job = this.checkJobExist(id);
        job.setStatus(JobConstant.JOB_STATUS_RUNNING);
        super.updateById(job);
        ScheduleUtil.resume(id);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateJob(Job job) {
        job.setStatus(JobConstant.JOB_STATUS_RUNNING);
        super.updateById(job);
        ScheduleUtil.update(job.getId(), job);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteJob(Long id) {
        super.removeById(id);
        ScheduleUtil.delete(id);
    }

    public Job checkJobExist(Long id) {
        Job job = super.getById(id);
        if (job == null) {
            throw new RuntimeException("不存在的任务ID");
        }
        return job;
    }
}

1.1.10 测试

基本代码已编写完成,开始测试。先使用postman工具调用添加定时任务的接口,新增之前编写的测试定时任务调度类的数据,设置每5秒执行一次,由于代码里写的逻辑是添加的时候只是创建好定时任务数据,并不会执行,如下图所示,成功创建数据。
调用添加接口图

接着调用启动定时任务接口,在查看控制台输出,每5秒输出一次,证明定时任务调度成功。
调用启动定时任务接口图
控制台输出图

在调用暂停定时任务接口,查看控制台输出,发现已没有在输出,证明成功暂停定时任务调度。

1.2 SpringBoot集成方式

使用SpringBoot的集成方式,那两个核心的类就不需要写了,因为 spring-boot-starter-quartz 已经帮我们整理完成。所以就不需要我们手动写配置了,其集成步骤就变成: pom文件添加依赖 ==》yml文件配置 ==》业务逻辑直接使用

1.2.1 添加pom依赖

<!--spring boot集成quartz定时任务-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

1.2.2 yml文件配置

上面介绍spring集成方式的时候quartz参数也是可以写在配置文件里的。

quartz:
      #相关属性配置
      properties:
        org:
          quartz:
            scheduler:
              instanceName: JavaSjzlScheduler
              instanceId: AUTO
            jobStore:
              class: org.quartz.impl.jdbcjobstore.JobStoreTX
              driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
              tablePrefix: QRTZ_
              isClustered: true
              clusterCheckinInterval: 10000
              useProperties: false
            threadPool:
              class: org.quartz.simpl.SimpleThreadPool
              threadCount: 10
              threadPriority: 5
              threadsInheritContextClassLoaderOfInitializingThread: true
      #数据库方式
      job-store-type: jdbc

1.2.3 业务逻辑直接使用

与spring集成方式相比,springBoot集成方式是自动配置的,但是其他业务逻辑代码还是一样的,这里就不在啰嗦,可以直接使用上面的代码。把SchedulerConfig 配置类注释掉,重启项目就可以了。在调用新增接口,启动接口,查看控制台输出,每隔5秒成功输出。

2.使用xxl-job框架创建

关于使用xxl-job框架创建的方式我们下一篇文章在介绍。敬请期待!

总结

好了,以上就是今天要讲的内容,本文介绍了单机环境和分布式环境下,创建定时任务的几种方式。重点介绍了Spring集成Quartz框架,并持久化定时任务到数据库中。

感谢大家的阅读,如果有什么疑问或者建议,欢迎评论区留下你的见解~

本次demo源码:

Gitee地址

Github地址