spring实现轻量级的可以动态添加删除的定时任务


前言

    定时任务在我们项目中经常被用到,我们在 springboot项目中,在启动类加上@EnableScheduling 注解,然后在定时任务的方法中加上@Scheduled(cron = “*/15 * * * * ?”)这样的注解,就可以很简单实现了一个定时任务

@SpringBootApplication
@EnableScheduling
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
		System.out.println("start success");
	}
	
	// 每15秒执行一次
	@Scheduled(cron = "*/15 * * * * ?")
	public void t() {
		System.out.println("123");
	}
}

    上面的定时任务方式,大家肯定都很熟悉。但是这里的定时任务调度方式只能是固定的,cron表达式运行了就不能更改。

    下面我们来看下,如果要动态添加删除定时任务,我们该怎么做?提到任务调度系统,那大名鼎鼎的quartz框架应该是出镜率比较高的了,我们这里先不研究quartz框架,就看用spring task怎么实现动态任务调度。
    实现的所有代码都在我gitee中 我的gitee
    我原来公司的CRM系统里面就使用了该框架进行任务的调度执行


一、优点

  1. 代码轻量。不要依赖其他框架或包
  2. 使用简单。直接调用addOrUpdateJob即可
  3. 进程内调度,无需依赖第三方程序
  4. 可以实现数据持久化。可以把任务状态等属性保存到数据库中

二、缺点

  1. 没有实现集群。会产生单点问题
  2. 调度方式比较简单
  3. 未实现监控统计功能

下面我们就用代码的方式来看怎么实现

三、代码实现

1、SchedulerTypeEnum(任务调度方式)

    该类是表示任务调度方式的,包含

  • 单次执行
  • cron表达式
  • 固定周期
  • 固定延迟
package com.hp.springboot.scheduler.enums;

/**
 * 描述:任务调度方式
 * 作者:黄平
 * 时间:2021年3月26日
 */
public enum SchedulerTypeEnum {

	ONCE(1, "单次执行"),
	CRON_TRIGGER(2, "cron表达式"),
	FIXED_RATE(3, "固定周期"),
	FIXED_DELAY(4, "固定延迟")
	;
	
	private int value;
	private String text;
	
	private SchedulerTypeEnum(int value, String text) {
		this.value = value;
		this.text = text;
	}

	public int getValue() {
		return value;
	}

	public String getText() {
		return text;
	}
}

这里的固定周期和固定延时的意思是:
1、固定周期:每间隔固定的时间就会执行(固定时间会包含程序执行时间)。所以可能会出现这样问题。第一次任务还没完成,第二次任务又开始了
2、固定延迟:每次任务执行完成了,才开始计算延迟时间。所以这个就能保证两次任务不会重叠
其实可以简单认为,固定周期计算时间从任务执行开始就计算,而固定延迟是从任务执行结束才开始计算

2、JobBean(任务调度对象)

package com.hp.springboot.scheduler;

import java.io.Serializable;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;

import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.support.CronTrigger;

import com.hp.springboot.common.util.DateUtil;
import com.hp.springboot.scheduler.enums.SchedulerTypeEnum;

/**
 * 
 * 描述:调度任务的对象
 * 作者:黄平
 * 时间:2016年6月18日
 */
public class JobBean implements Serializable {

	private static final long serialVersionUID = -156296402817061433L;

	/**
	 * 任务名称(必须唯一)
	 */
	private String jobName;

	/**
	 * 任务调度方式
	 */
	private SchedulerTypeEnum scheduleType;
	
	/**
	 * trigger任务的触发器
	 */
	private Trigger trigger;
	
	/**
	 * 开始时间
	 */
	private Date startTime;
	
	/**
	 * 固定周期(毫秒)
	 */
	private long period;
	
	/**
	 * 固定延迟(毫秒)
	 */
	private long delay;
	
	/**
	 * 任务线程
	 */
	private Runnable task;
	
	/**
	 * 定时任务返回对象
	 */
	private ScheduledFuture<?> scheduledFuture;

	private JobBean() {}
	
	/**
	 * @Title: createOnceJob
	 * @Description: 创建只执行一次的任务对象
	 * @param jobName
	 * @param startTime
	 * @param task
	 * @return
	 */
	public static JobBean createOnceJob(String jobName, Date startTime, Runnable task) {
		JobBean job = new JobBean();
		job.setScheduleType(SchedulerTypeEnum.ONCE);
		job.setJobName(jobName);
		job.setStartTime(startTime);
		job.setTask(task);
		return job;
	}
	
	/**
	 * @Title: createOnceJob
	 * @Description: 创建只执行一次的任务对象
	 * @param jobName
	 * @param startTime(时间戳,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createOnceJob(String jobName, long startTime, Runnable task) {
		Date stime = DateUtil.longToDate(startTime);
		return createOnceJob(jobName, stime, task);
	}
	
	/**
	 * @Title: createCronJob
	 * @Description: 创建触发器任务对象
	 * @param jobName
	 * @param trigger
	 * @param task
	 * @return
	 */
	public static JobBean createCronJob(String jobName, Trigger trigger, Runnable task) {
		JobBean job = new JobBean();
		job.setScheduleType(SchedulerTypeEnum.CRON_TRIGGER);
		job.setJobName(jobName);
		job.setTask(task);
		job.setTrigger(trigger);
		return job;
	}
	
	/**
	 * @Title: createCronJob
	 * @Description: 创建触发器任务对象
	 * @param jobName
	 * @param cronExpression
	 * @param task
	 * @return
	 */
	public static JobBean createCronJob(String jobName, String cronExpression, Runnable task) {
		Trigger trigger = new CronTrigger(cronExpression);
		return createCronJob(jobName, trigger, task);
	}
	
	/**
	 * @Title: createFixedRateJob
	 * @Description: 创建固定周期任务对象
	 * @param jobName
	 * @param startTime
	 * @param period(周期时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedRateJob(String jobName, Date startTime, long period, Runnable task) {
		JobBean job = new JobBean();
		job.setScheduleType(SchedulerTypeEnum.FIXED_RATE);
		job.setJobName(jobName);
		job.setTask(task);
		job.setStartTime(startTime);
		job.setPeriod(period);
		return job;
	}
	
	/**
	 * @Title: createFixedRateJob
	 * @Description: 创建固定周期任务对象
	 * @param jobName
	 * @param startTime(时间戳,毫秒)
	 * @param period(周期时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedRateJob(String jobName, long startTime, long period, Runnable task) {
		Date stime = startTime == 0 ? null : DateUtil.longToDate(startTime);
		return createFixedRateJob(jobName, stime, period, task);
	}
	
	/**
	 * @Title: createFixedRateJob
	 * @Description: 创建固定周期任务对象
	 * @param jobName
	 * @param period(周期时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedRateJob(String jobName, long period, Runnable task) {
		return createFixedRateJob(jobName, 0, period, task);
	}
	
	/**
	 * @Title: createFixedDelayJob
	 * @Description: 创建固定延迟时间的任务对象
	 * @param jobName
	 * @param startTime
	 * @param delay(延迟时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedDelayJob(String jobName, Date startTime, long delay, Runnable task) {
		JobBean job = new JobBean();
		job.setScheduleType(SchedulerTypeEnum.FIXED_DELAY);
		job.setJobName(jobName);
		job.setTask(task);
		job.setStartTime(startTime);
		job.setDelay(delay);
		return job;
	}
	
	/**
	 * @Title: createFixedDelayJob
	 * @Description: 创建固定延迟时间的任务对象
	 * @param jobName
	 * @param startTime(时间戳,毫秒)
	 * @param delay(延迟时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedDelayJob(String jobName, long startTime, long delay, Runnable task) {
		Date stime = startTime == 0 ? null : DateUtil.longToDate(startTime);
		return createFixedDelayJob(jobName, stime, delay, task);
	}
	
	/**
	 * @Title: createFixedDelayJob
	 * @Description: 创建固定延迟时间的任务对象
	 * @param jobName
	 * @param delay(延迟时间,毫秒)
	 * @param task
	 * @return
	 */
	public static JobBean createFixedDelayJob(String jobName, long delay, Runnable task) {
		return createFixedDelayJob(jobName, 0, delay, task);
	}

	get and set...

    该类是调度任务的对象。包含了任务名称(唯一),调度方式,具体任务。另外还提供了快速创建对应的单次,cron,固定周期,固定延迟的任务方法。

3、SchedulerJob(任务调度)

package com.hp.springboot.scheduler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;

import com.hp.springboot.scheduler.enums.SchedulerTypeEnum;

/**
 * 描述:调度任务
 * 作者:黄平
 * 时间:2021年3月26日
 */
public class SchedulerJob {

	private static Logger log = LoggerFactory.getLogger(SchedulerJob.class);
	
	/**
	 * 保存所有的定时任务
	 * 使用ConcurrentHashMap保证线程安全
	 */
	private static Map<String, JobBean> JOB_MAP = new ConcurrentHashMap<>();
	
	@Autowired
	private TaskScheduler taskScheduler;
	
	/**
	 * 添加或修改任务
	 * @param job
	 */
	public void addOrUpdateJob(JobBean job) {
		log.info("addOrUpdateJob with job={}", job);
		if (job == null) {
			log.warn("addOrUpdateJob error. job is null");
			return;
		}
		if (StringUtils.isEmpty(job.getJobName())) {
			log.warn("addOrUpdateJob error. jobName is empty with job={}", job);
			return;
		}
		JobBean oldJob = JOB_MAP.get(job.getJobName());
		if (oldJob != null) {
			// 任务存在,判断是否修改了属性
			if (jobEquals(oldJob, job)) {
				// 任务属性一样,没有改变,则直接退出
				return;
			}
			//任务修改了时间属性,则停止掉以前的任务
			cancel(oldJob, false);
		}
		
		// 开始新任务
		startJob(job);
	}
	
	/**
	 * @Title: cancel
	 * @Description: 关闭任务
	 * @param job
	 * @param force
	 * @return
	 */
	public boolean cancel(JobBean job, boolean force) {
		// 停止任务
		boolean result = job.getScheduledFuture().cancel(force);
		
		// 从map中删除
		JOB_MAP.remove(job.getJobName());
		return result;
	}
	
	/**
	 * @Title: cancel
	 * @Description: 关闭定时任务
	 * @param jobName
	 * @param force
	 * @return
	 */
	public boolean cancel(String jobName, boolean force) {
		JobBean o = JOB_MAP.get(jobName);
		if (o == null) {
			return true;
		}
		return cancel(o, force);
	}
	
	/**
	 * @Title: startJob
	 * @Description: 开始新任务
	 * @param job
	 * @return
	 */
	private ScheduledFuture<?> startJob(JobBean job) {
		ScheduledFuture<?> future = null;
		SchedulerTypeEnum schedulerType = job.getScheduleType();
		if (SchedulerTypeEnum.ONCE.equals(schedulerType)) {
			// 一次任务
			future = taskScheduler.schedule(job.getTask(), job.getStartTime());
		} else if (SchedulerTypeEnum.CRON_TRIGGER.equals(schedulerType)) {
			// cron 表达式
			future = taskScheduler.schedule(job.getTask(), job.getTrigger());
		} else if (SchedulerTypeEnum.FIXED_RATE.equals(schedulerType)) {
			// 固定周期
			if (job.getStartTime() == null) {
				future = taskScheduler.scheduleAtFixedRate(job.getTask(), job.getPeriod());
			} else {
				future = taskScheduler.scheduleAtFixedRate(job.getTask(), job.getStartTime(), job.getPeriod());
			}
		} else if (SchedulerTypeEnum.FIXED_DELAY.equals(schedulerType)) {
			// 固定延迟
			if (job.getStartTime() == null) {
				future = taskScheduler.scheduleWithFixedDelay(job.getTask(), job.getDelay());
			} else {
				future = taskScheduler.scheduleWithFixedDelay(job.getTask(), job.getStartTime(), job.getDelay());
			}
		} else {
			return null;
		}
		
		//任务存入缓存
		job.setScheduledFuture(future);
		JOB_MAP.put(job.getJobName(), job);
		return future;
	}
	
	/**
	 * @Title: jobEquals
	 * @Description: 检查两个任务是否一样
	 * @param oldJob
	 * @param newJob
	 * @return
	 */
	private boolean jobEquals(JobBean oldJob, JobBean newJob) {
		if (oldJob.getScheduleType() != newJob.getScheduleType()) {
			// 任务调度方式改变了,则直接返回
			return false;
		}
		
		// 按照任务调度方式来分别判断
		SchedulerTypeEnum schedulerType = oldJob.getScheduleType();
		if (SchedulerTypeEnum.ONCE.equals(schedulerType)) {
			// 一次性任务,判断开始时间是否变化
			if (oldJob.getStartTime() == null) {
				return newJob.getStartTime() == null;
			} else {
				return oldJob.getStartTime().equals(newJob.getStartTime());
			}
		} else if (SchedulerTypeEnum.CRON_TRIGGER.equals(schedulerType)) {
			// cron 表达式
			if (oldJob.getTrigger() == null) {
				return newJob.getTrigger() == null;
			} else {
				return oldJob.getTrigger().equals(newJob.getTrigger());
			}
		} else if (SchedulerTypeEnum.FIXED_RATE.equals(schedulerType)) {
			// 固定周期
			if (oldJob.getStartTime() == null) {
				return newJob.getStartTime() == null && oldJob.getPeriod() == newJob.getPeriod();
			} else {
				return oldJob.getStartTime().equals(newJob.getStartTime()) && oldJob.getPeriod() == newJob.getPeriod();
			}
		} else if (SchedulerTypeEnum.FIXED_DELAY.equals(schedulerType)) {
			// 固定延迟
			if (oldJob.getStartTime() == null) {
				return newJob.getStartTime() == null && oldJob.getDelay() == newJob.getDelay();
			} else {
				return oldJob.getStartTime().equals(newJob.getStartTime()) && oldJob.getDelay() == newJob.getDelay();
			}
		}
		return false;
	}
}

    该类是具体任务调度的对象。里面提供了添加或修改任务(修改其实就是先删除,再新增),取消任务方法。

1、使用了ConcurrentHashMap来保存所有的任务信息,并且保证了线程安全,
2、taskScheduler对象需要在spring bean里面定义

添加任务方法addOrUpdateJob方法

1、首先判断job对象和jobName是否为空,为空则直接退出。
2、从JOB_MAP中获取该任务是否已经存在
    2.1、如果已经存在,判断是否修改了任务属性(使用jobEquals方法判断)
    2.2、如果任务已经存在,而且任务属性没有修改,则直接退出,不做任何操作
    2.3、如果任务存在,而且修改了任务属性,则删除掉旧的任务
3、新建新的任务
    3.1、根据任务调度方式创建新的定时任务,并且返回ScheduledFuture对象保存在jobBean对象中
    3.2、新任务保存到JOB_MAP对象中

删除任务方法cancel

1、直接调用 job.getScheduledFuture().cancel(force) 方法取消任务。force为是否强制取消(true:如果任务正在执行,则直接终止,可能会出现异常)
2、再从JOB_MAP中删除该任务对象

4、SchedulerConfiguration(springboot启动配置类)

    为了使该定时任务框架在启动服务时就可以直接使用,那我们还需要配置一个springboot配置类

package com.hp.springboot.scheduler;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

/**
 * 描述:定时任务的配置类
 * 作者:黄平
 * 时间:2021年3月30日
 */
@Configuration
public class SchedulerConfiguration {

	/**
	 * 执行定时任务的线程池大小
	 */
	@Value("${hp.springboot.scheduler.pool-size:10}")
	private int poolSize;
	
	/**
	 * @Title: taskScheduler
	 * @Description: 生成TaskScheduler
	 * 这里使用了线程池的方式;还可以自定义TaskScheduler
	 * @return
	 */
	@Bean
	@ConditionalOnMissingBean(TaskScheduler.class)
	public TaskScheduler taskScheduler() {
		ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
		taskScheduler.setPoolSize(poolSize);
		taskScheduler.setThreadNamePrefix("scheduler-");
		taskScheduler.initialize();
		return taskScheduler;
	}
	
	@Bean
	public SchedulerJob schedulerJob() {
		return new SchedulerJob();
	}
}

1、poolSize是默认使用了ThreadPoolTaskScheduler 线程池进行调度的线程池大小
2、taskScheduler方法是创建了一个默认的线程池调度
3、schedulerJob就是把SchedulerJob 交给spring容器管理,我们可以在项目中直接注入他

然后我们使用spring的自动扫描技术,在我们的resources/META-INF/下创建spring.factories文件,内容

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hp.springboot.scheduler.SchedulerConfiguration

把上面的代码达成jar包,然后在我们项目中只要依赖这个包,就可以直接使用定时任务调度了。
使用方法

	@Autowired
	private SchedulerJob schedulerJob;
	
	@RequestMapping("/start")
	public void start(String jobName) {
		schedulerJob.addOrUpdateJob(JobBean.createFixedDelayJob(jobName, 2000, new Runnable() {
			@Override
			public void run() {
				log.info(jobName);
			}
		}));
	}
	
	@RequestMapping("/stop")
	public void stop(String jobName) {
		schedulerJob.cancel(jobName, false);
	}

如果要实现数据持久化的话,只要在addOrUpdateJob方法里面把数据同步一份到数据库,然后在每次服务启动的时候,从数据库加载已经存在的任务即可

注意:
如果你的系统是集群部署的话,那需要在任务里面或者在框架里面使用分布式锁或其他方式保证任务只在一台机上运行。不然会出现意想不到的问题

这里我们可以做一个简单一个页面来控制任务新建,运行,取消等等。这里就不展开继续写了

这样一个简单的可以动态添加定时任务的功能就完成了,是不是很简单。其实有很多开源的定时任务框架或解决方案,我这里只是提供一种思路,把我所做的或遇到的问题和解决方案和大家分享。欢迎大家一起讨论。
我的所有项目到在我的gitee 我的gitee
上面定时任务调用框架地址 动态定时任务调度框架

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值