定时任务
一、@Scheduled注解
1.1简介
@Scheduled注解是Spring Boot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行。它的内部实现是基于 java 中的 ScheduledThreadPoolExecutor 类。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.2使用
启动类上添加 @EnableScheduling 注解
package com.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication(scanBasePackages = "com.demo")
@MapperScan("com.demo.dao")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("------服务启动完成------");
}
}
1.3Cron表达式
1.格式:
秒 分 时 日 月 周 年(可为空)
2.取值
位置 | 取值 |
---|---|
秒 | 0-59 |
分 | 0-59 |
时 | 0-24 |
日 | 1-31 |
月 | 1-12 |
星期 | 1-7 |
年 | 可为空,1970-2099,若为空,表示全部时间范围 |
3.字符含义
字符 | 解释 |
---|---|
* | 每的意思。在不同的字段上,就代表每秒,每分,每小时等。如果在“日”这个域中设置 *,表示每一天都会触发。 |
- | 指定值的范围。这个比较好理解就是指定在某个域的连续范围,如果我们在 “时” 这个域中定义 1-6,则表示在1到6点之间每小时都触发一次,用 , 表示 1,2,3,4,5,6 |
, | 这里指的是在两个以上的时间点中都执行,如果我们在 “分” 这个域中定义为 8,12,35 ,则表示分别在第8分,第12分 第35分执行该定时任务。 |
/ | 表示起始时间开始触发,而后每隔固定时间触发一次。在某个域上周期性触发,该符号将其所在域中的表达式分为两个部分,其中第一部分是起始值,除了秒以外都会降低一个单位,比如 在 “秒” 上定义 5/10 表示从 第 5 秒开始 每 10 秒执行一次,而在 “分” 上则表示从 第 5 秒开始 每 10 分钟执行一次。 |
? | 仅用于【日】和【周】字段。因为在指定某日和周几的时候,这两个值实际上是冲突的,所以需要用【?】标识不生效的字段。比如【0 1 * * * ?】就代表每年每月每日每小时的1分0秒触发任务。这里的周就没有效果了。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的8号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ? |
L | 表示英文中的LAST 的意思,只能在 “日”和“周”中使用。在“日”中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在“周”上表示周六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在“周”上设置”7L”这样的格式,则表示“本月最后一个周六” |
W | 表示离指定日期的最近那个工作日(周一至周五)触发,只能在 “日” 中使用且只能用在具体的数字之后。若在“日”上置”15W”,表示离每月15号最近的那个工作日触发。假如15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果是 “1W” 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。 |
# | 表示每月的第几个周几,只能作用于 “周” 上。例如 ”2#3” 表示在每月的第三个周二。 |
1.4结果
package com.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TestTask {
/**
* cron:自定义规则
* 每10秒执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
private void test1() {
log.info("test1每10秒执行一次");
}
}
package com.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TestTask {
/**
* cron:自定义规则
* 每10秒执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
private void test1() {
log.info("test1每10秒执行一次");
}
/**
* fixedDelay:固定延迟
* 延迟执行任务。任务在上个任务完成后达到设置的延时时间就执行。
*/
@Scheduled(fixedDelay = 5000)
private void test2() {
log.info("test2任务会在上个任务完成后经过5s再执行");
}
/**
* fixedRate:定时执行
* 定时执行。任务间隔规定时间即执行。
* 此处,任务每隔五秒便会执行一次。
*/
@Scheduled(fixedRate = 5000)
private void test3() {
log.info("test3任务每隔五秒便会执行一次");
}
}
二、ruoyi-vue框架定时任务(ruoyi-quartz )
首先我们可以看到有这样的一个界面,可以调控定时任务。
2.1 如何操作
2.2 解析
代码详情解析参考:B站博主:一写代码就开心
所有代码可以去若依官方下载
2.2.1 第三方依赖
<!-- 定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<exclusions>
<exclusion>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
</exclusions>
</dependency>
2.2.2 两张表
定时任务调度表 sys_job
定时任务调度日志表 sys_job_log
对应的实体类
2.2.3 工具类
AbstractQuartzJob
package com.ruoyi.quartz.util;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.utils.ExceptionUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.quartz.domain.SysJob;
import com.ruoyi.quartz.domain.SysJobLog;
import com.ruoyi.quartz.service.ISysJobLogService;
/**
* 抽象quartz调用
* 这个是抽象类,需要有具体的实现类。QuartzDisallowConcurrentExecution QuartzJobExecution
* @author ruoyi
*/
public abstract class AbstractQuartzJob implements Job
{
private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
/**
* 线程本地变量
* 里面保存 日期
* 保证每一个线程 都有一个自己的时间
*/
private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();
@Override
public void execute(JobExecutionContext context) throws JobExecutionException
{
SysJob sysJob = new SysJob();
BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
try
{
//记录当前任务的开始时间
before(context, sysJob);
if (sysJob != null)
{
//执行
doExecute(context, sysJob);
}
//执行完成之后,记录执行日志。没有异常。
after(context, sysJob, null);
}
catch (Exception e)
{
log.error("任务执行异常 - :", e);
after(context, sysJob, e);
}
}
/**
* 执行前
*
* @param context 工作执行上下文对象
* @param sysJob 系统计划任务
*/
protected void before(JobExecutionContext context, SysJob sysJob)
{
threadLocal.set(new Date());
}
/**
* 执行后
*
* @param context 工作执行上下文对象
* @param sysJob 系统计划任务
*/
protected void after(JobExecutionContext context, SysJob sysJob, Exception e)
{
//从本地线程 获取到 当前任务开始执行时间
Date startTime = threadLocal.get();
//获取之后,删除掉
threadLocal.remove();
final SysJobLog sysJobLog = new SysJobLog();
//任务名称
sysJobLog.setJobName(sysJob.getJobName());
//执行器
sysJobLog.setJobGroup(sysJob.getJobGroup());
//执行的方法名称
sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
//任务执行的开始时间
sysJobLog.setStartTime(startTime);
//任务执行的结束时间
sysJobLog.setStopTime(new Date());
//总耗时
long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
//判断当前任务 在执行过程中 是否报错
if (e != null)
{
//任务执行有错
sysJobLog.setStatus(Constants.FAIL);
String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000);
sysJobLog.setExceptionInfo(errorMsg);
}
else
{
//任务执行没错
sysJobLog.setStatus(Constants.SUCCESS);
}
// 写入数据库当中
// 从配置类中获取ISysJobLogService这个bean对象
// 使用bean对象 添加日志信息
SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog);
}
/**
* 执行方法,由子类重载
*
* @param context 工作执行上下文对象
* @param sysJob 系统计划任务
* @throws Exception 执行过程中的异常
*/
protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
}
QuartzDisallowConcurrentExecution、QuartzJobExecution
重写了AbstractQuartzJob中的doExecute方法,区别在于是否并发执行。
QuartzDisallowConcurrentExecution
package com.ruoyi.quartz.util;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import com.ruoyi.quartz.domain.SysJob;
/**
* 定时任务处理(禁止并发执行)
*
* @author ruoyi
*
*/
@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob
{
@Override
protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
{
JobInvokeUtil.invokeMethod(sysJob);
}
}
QuartzJobExecution
package com.ruoyi.quartz.util;
import org.quartz.JobExecutionContext;
import com.ruoyi.quartz.domain.SysJob;
/**
* 定时任务处理(允许并发执行)
*
* @author ruoyi
*
*/
public class QuartzJobExecution extends AbstractQuartzJob
{
@Override
protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
{
JobInvokeUtil.invokeMethod(sysJob);
}
}
CronUtils
package com.ruoyi.quartz.util;
import java.text.ParseException;
import java.util.Date;
import org.quartz.CronExpression;
/**
* cron表达式工具类
*
* @author ruoyi
*
*/
public class CronUtils
{
/**
* 返回一个布尔值代表一个给定的Cron表达式的有效性
*
* @param cronExpression Cron表达式
* @return boolean 表达式是否有效
*/
public static boolean isValid(String cronExpression)
{
//直接使用第三方方法进行判断
return CronExpression.isValidExpression(cronExpression);
}
/**
* 返回一个字符串值,表示该消息无效Cron表达式给出有效性
*
* @param cronExpression Cron表达式
* @return String 无效时返回表达式错误描述,如果有效返回null
*/
public static String getInvalidMessage(String cronExpression)
{
try
{
new CronExpression(cronExpression);
return null;
}
catch (ParseException pe)
{
return pe.getMessage();
}
}
/**
* 返回下一个执行时间根据给定的Cron表达式
*
* @param cronExpression Cron表达式
* @return Date 下次Cron表达式执行时间
*/
public static Date getNextExecution(String cronExpression)
{
try
{
//创建corn 表达式的对象
CronExpression cron = new CronExpression(cronExpression);
//第三方发方法 从当前时间开始计算 获取到他下一次的执行时间
return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
}
catch (ParseException e)
{
throw new IllegalArgumentException(e.getMessage());
}
}
}
JobInvokeUtil
package com.ruoyi.quartz.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.quartz.domain.SysJob;
/**
* 任务执行工具
*
* @author ruoyi
*/
public class JobInvokeUtil
{
/**
* 执行方法
*
* @param sysJob 系统任务
*/
public static void invokeMethod(SysJob sysJob) throws Exception
{
//ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)
//就是调用目标字符串
String invokeTarget = sysJob.getInvokeTarget();
//根据 要执行的方法 获取到bean名称
//ryTask
String beanName = getBeanName(invokeTarget);
//根据 要执行的方法 获取到methodName
//ryMultipleParams
String methodName = getMethodName(invokeTarget);
//获取方法的参数
List<Object[]> methodParams = getMethodParams(invokeTarget);
//判断这个 bean对象在不在
if (!isValidClassName(beanName))
{
//根据名称获取bean对象
Object bean = SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
}
else
{
//创建bean对象
Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
/**
* 调用任务方法
*
* @param bean 目标对象
* @param methodName 方法名称
* @param methodParams 方法参数
*/
private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)
{
//反射 获取到具体的方法的对象
Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
//执行具体方法
method.invoke(bean, getMethodParamsValue(methodParams));
}
else
{
Method method = bean.getClass().getMethod(methodName);
method.invoke(bean);
}
}
/**
* 校验是否为为class包名
*
* @param invokeTarget 名称
* @return true是 false否
*/
public static boolean isValidClassName(String invokeTarget)
{
return StringUtils.countMatches(invokeTarget, ".") > 1;
}
/**
* 获取bean名称
*
* @param invokeTarget 目标字符串
* @return bean名称
*/
public static String getBeanName(String invokeTarget)
{
//ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)
//ryTask.ryMultipleParams
String beanName = StringUtils.substringBefore(invokeTarget, "(");
//ryTask
return StringUtils.substringBeforeLast(beanName, ".");
}
/**
* 获取bean方法
*
* @param invokeTarget 目标字符串
* @return method方法
*/
public static String getMethodName(String invokeTarget)
{
//ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)
//ryTask.ryMultipleParams
String methodName = StringUtils.substringBefore(invokeTarget, "(");
//ryMultipleParams
return StringUtils.substringAfterLast(methodName, ".");
}
/**
* 获取method方法参数相关列表
*
* @param invokeTarget 目标字符串
* @return method方法相关参数列表
*/
public static List<Object[]> getMethodParams(String invokeTarget)
{
//ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)
//'ry', true, 2000L, 316.50D, 100
String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
if (StringUtils.isEmpty(methodStr))
{
return null;
}
//以,隔开 转为数组
String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)");
//返回list集合
List<Object[]> classs = new LinkedList<>();
for (int i = 0; i < methodParams.length; i++)
{
//去空格
String str = StringUtils.trimToEmpty(methodParams[i]);
// String字符串类型,以'或"开头
if (StringUtils.startsWithAny(str, "'", "\""))
{
classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class });
}
// boolean布尔类型,等于true或者false
else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str))
{
classs.add(new Object[] { Boolean.valueOf(str), Boolean.class });
}
// long长整形,以L结尾
else if (StringUtils.endsWith(str, "L"))
{
classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class });
}
// double浮点类型,以D结尾
else if (StringUtils.endsWith(str, "D"))
{
classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class });
}
// 其他类型归类为整形
else
{
classs.add(new Object[] { Integer.valueOf(str), Integer.class });
}
}
return classs;
}
/**
* 获取参数类型
*
* @param methodParams 参数相关列表
* @return 参数类型列表
*/
public static Class<?>[] getMethodParamsType(List<Object[]> methodParams)
{
Class<?>[] classs = new Class<?>[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Class<?>) os[1];
index++;
}
return classs;
}
/**
* 获取参数值
*
* @param methodParams 参数相关列表
* @return 参数值列表
*/
public static Object[] getMethodParamsValue(List<Object[]> methodParams)
{
Object[] classs = new Object[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Object) os[0];
index++;
}
return classs;
}
}
ScheduleUtils
package com.ruoyi.quartz.util;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.exception.job.TaskException;
import com.ruoyi.common.exception.job.TaskException.Code;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.quartz.domain.SysJob;
/**
* 定时任务工具类
*
* @author ruoyi
*
*/
public class ScheduleUtils
{
/**
* 得到quartz任务类
*
* @param sysJob 执行计划
* @return 具体执行任务类
*/
private static Class<? extends Job> getQuartzJobClass(SysJob sysJob)
{
/** 是否并发执行 (0允许 1禁止) 用QuartzJobExecution还是用QuartzDisallowConcurrentExecution */
boolean isConcurrent = "0".equals(sysJob.getConcurrent());
return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
}
/**
* 构建任务触发对象
*/
public static TriggerKey getTriggerKey(Long jobId, String jobGroup)
{
return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 构建任务键对象
*/
public static JobKey getJobKey(Long jobId, String jobGroup)
{
return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 创建定时任务
* @param scheduler 第三方jar包里的,就是使用它实现定时
* 新增定时任务时
*/
public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException
{
//是否并发执行 (0允许 1禁止)
Class<? extends Job> jobClass = getQuartzJobClass(job);
// 构建job信息
//任务ID
Long jobId = job.getJobId();
//任务组ID
String jobGroup = job.getJobGroup();
JobDetail jobDetail = JobBuilder.newJob(jobClass)//定义任务类 为特定类 (哪个文件要执行,执行的是允许并发的还是静止并发的)
.withIdentity(getJobKey(jobId, jobGroup)).build();//定义唯一标识 就是定义name/group (具体执行哪个任务)
// 表达式调度构建器
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
//设置定时任务执行策略
cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
.withSchedule(cronScheduleBuilder).build();
// 放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
// 判断是否存在
if (scheduler.checkExists(getJobKey(jobId, jobGroup)))
{
// 防止创建时存在数据问题 先移除,然后在执行创建操作
scheduler.deleteJob(getJobKey(jobId, jobGroup));
}
// 判断任务是否过期
if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression())))
{
// 执行调度任务
//将jobDetail和trigger 加入这个调度
scheduler.scheduleJob(jobDetail, trigger);
}
// 暂停任务
//任务状态(0正常 1暂停)
if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue()))
{
scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
}
}
/**
* 设置定时任务策略
*/
public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
throws TaskException
{
switch (job.getMisfirePolicy())
{
case ScheduleConstants.MISFIRE_DEFAULT:
return cb;
case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
return cb.withMisfireHandlingInstructionIgnoreMisfires();
case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
return cb.withMisfireHandlingInstructionFireAndProceed();
case ScheduleConstants.MISFIRE_DO_NOTHING:
return cb.withMisfireHandlingInstructionDoNothing();
default:
throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+ "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR);
}
}
/**
* 检查包名是否为白名单配置
*
* @param invokeTarget 目标字符串
* @return 结果
*/
public static boolean whiteList(String invokeTarget)
{
//ryTask.ryMultipleParams('ry', true, 2000L, 316.50D, 100)
//ryTask.ryMultipleParams
String packageName = StringUtils.substringBefore(invokeTarget, "(");
int count = StringUtils.countMatches(packageName, ".");
if (count > 1)
{
return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);
}
//ryTask
Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
String beanPackageName = obj.getClass().getPackage().getName();
return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR)
&& !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR);
}
}