若依项目学习笔记11——定时任务
1. 定时任务
在Web应用中,往往有一类功能是不可缺少的,那就是定时任务。 定时任务的应用非常广泛,如某些视频网站,每天会给会员送成长值,每月电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 在本项目中,提供方便友好的web界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程
1.1 属性说明
上图中,容易理解的就不介绍了,就讲讲比较重点的
1.1.1 调用方法
Bean调用示例:ryTask.ryParams('ry');Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry');参数说明:支持字符串,布尔类型,长整型,浮点型,整型
- 字符串(需要单引号''标识 如:ryTask.ryParams(’ry’))
- 布尔类型(需要true false标识 如:ryTask.ryParams(true))
- 长整型(需要L标识 如:ryTask.ryParams(2000L))
- 浮点型(需要D标识 如:ryTask.ryParams(316.50D))
- 整型(纯数字即可)
1.1.2 cron表达式语法
[秒] [分] [小时] [日] [月] [周] [年]
说明 | 必填 | 范围 | 通配符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
小时 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * / |
月 | 是 | 1-12 / JAN-DEC | , - * ? / L W |
周 | 是 | 1-7 or SUN-SAT | , - * ? / L # |
年 | 是 | 1970-2099 | , - * / |
通配符说明
- * :所有值。 如:在分上设置 *,表每一分钟都会触发
- ? :不指定值。使用的场景为不需要关心当前设置这个字段的值。如:每月10号触发一个操作,但不关心是周几,则需要周字段设置为”?” 具体设置为 0 0 0 10 * ?
- - :区间。如在小时上设置 “10-12”,表 10,11,12点都会触发
- , :指定多个值,如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
- / :用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次
- L :最后的意思。在日字段设置上,表当月最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
- W :离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)
- # :序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同
常用表达式例子
表达式 | 说明 |
---|---|
0 0 2 1 * ? * | 表示在每月的1日的凌晨2点调整任务 |
0 15 10 L * ? | 每月最后一日的上午10:15触发 |
0 0 12 * * ? | 每天中午12点触发 |
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时 |
0 15 10 ? * MON-FRI | 周一至周五的上午10:15触发 |
1.1.3 错误策略说明
- 立即执行: 例如整点执行的任务,系统在九点死机了,在十点十五恢复(以下均是),则执行两次整点任务(九点和十点的)
- 执行一次:只执行一次整点任务(十点的)
- 放弃执行:不执行九点和十点的,等着做十一点的
1.2 定时任务调度测试
我们找到 quartz.task 包,在其中创建一个流量定时任务类 FlowTask
,如下
@Service("flow")
public class FlowTask {
public void getFlowPackage(){
System.out.println("查询所有流量包");
}
public void getFlowById(String orderId){
System.out.println("查询流量:"+ orderId);
}
public void delFlowById(String orderId){
System.out.println("删除流量:"+ orderId);
}
}
然后启动项目,找到定时任务,创建一个查看全部流量包的定时任务,然后执行一次
控制台输出
我们试试查询指定订单的方法,注意方法参数用单引号包起来
结果
上面是用 Bean 方法来调用,我们也可以使用 Class 方式来调用,如 com.ruoyi.quartz.task.FlowTask.getFlowById('A2020123')
要开启定时任务,直接在状态属性部分点击开启,系统就会按照我们设置的cron表达式来执行;定时任务方法参数支持多参,字符串用 单引号 包起来,long 类型后面要加 L ,double 类型后面要加 D
删除也是同理,略''
1.3 定时任务代码详解
定时任务的数据库脚本存放在项目的sql文件夹的 quartz.sql 中,同时还要在quartz模块下添加依赖
<!-- 定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<exclusions>
<exclusion>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
</exclusions>
</dependency>
定时任务的配置在 quartz.config.ScheduleConfig ,大家自行看一下,配置完成后,项目启动便会执行初始化方法 quartz.service.impl.SysJobServiceImpl.init()
,主要是防止用户修改数据库sql文件,导致创建数据库时和代码不一致的问题;这个部分大家结合创建定时任务的界面内容来对比理解,实现步骤如下:
- 首先看到
createScheduleJob()
方法,下面还有一个getQuartzJobClass()
方法,这是用于控制是否并发,通过获取类名来判断是否是并发类(是并发,则类上用注解@DisallowConcurrentExecution
来禁止并发) - 然后将其添加到job对象中
JobBuilder.newJob(jobClass)
,再获取job的key(包括名称和分组)JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup)
- 再创建cron表达式调度构建器和错误策略
- 创建trigger,表示示例创建好了
- 放入参数
jobDetail.getJobDataMap()
- 调用API判断,如果数据库已存在,先删除
- 调用API判断是否暂停
scheduler.pauseJob()
接下来我们看一下运行流程,首先找到 quartz.controller.SysJobController.run()
方法,其中调用了一个 run()
的执行方法,我们找到其实现类 quartz.service.impl.SysJobServiceImpl.run()
@Override
@Transactional
public void run(SysJob job) throws SchedulerException
{
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
SysJob properties = selectJobById(job.getJobId());
// 参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties);
scheduler.triggerJob(ScheduleUtils.getJobKey(jobId, jobGroup), dataMap);
}
该方法传入一个job对象,调用 triggerJob()
方法来执行参数,其参数就是前面所创建的class(即实现过程1,对应数据库中qrtz_job_details表的job_class_name属性),我们看到开启并发类 QuartzJobExecution
,继承了 AbstractQuartzJob
,我们进去(可以看到继承了 Job
类),找到 execute()
方法,
@Override
public void execute(JobExecutionContext context) throws JobExecutionException
{
SysJob sysJob = new SysJob();
//获取数据目标key,其中存入了数据对象,保存了任务对象的信息,如id、名称等等,会保存到数据库的 sys_job 表和 sys_job_log 表
BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
try
{
//通过before和after来记录时间,记录耗时
before(context, sysJob);
if (sysJob != null)
{
doExecute(context, sysJob);
}
//设置相关信息并写入数据库日志表 sys_job_log
after(context, sysJob, null);
}
catch (Exception e)
{
log.error("任务执行异常 - :", e);
after(context, sysJob, e);
}
}
然后重写 doExecute()
方法,下面有两个子类,分别是 QuartzDisallowConcurrentExecution
和 QuartzJobExecution
,内容都是一样的,我们随便点进一个查看,可以看到有 JobInvokeUtil.invokeMethod(sysJob)
方法,用于执行具体目标类,我们进去看一下;
public static void invokeMethod(SysJob sysJob) throws Exception
{
//获取bean和method名称,通过检测 . 来获取bean名称,通过检测 ( 来获取方法名称
String invokeTarget = sysJob.getInvokeTarget();
String beanName = getBeanName(invokeTarget);
String methodName = getMethodName(invokeTarget);
//获取参数,getMethodParams() 对参数类型进行判断
List<Object[]> methodParams = getMethodParams(invokeTarget);
//通过 . 的数目来判断是bean还是class方式,最后都是执行invokeMethod方法来执行
if (!isValidClassName(beanName))
{
Object bean = SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
}
else
{
Object bean = Class.forName(beanName).newInstance();
invokeMethod(bean, methodName, methodParams);
}
下面我们看 invokeMethod()
方法
if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)//带参
{
//传入参数列表,再执行目标方法
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
method.invoke(bean, getMethodParamsValue(methodParams));
}
else//无参
{
//直接用反射机制执行目标方法
Method method = bean.getClass().getDeclaredMethod(methodName);
method.invoke(bean);
}