若依项目学习笔记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文件,导致创建数据库时和代码不一致的问题;这个部分大家结合创建定时任务的界面内容来对比理解,实现步骤如下:

  1. 首先看到createScheduleJob() 方法,下面还有一个 getQuartzJobClass() 方法,这是用于控制是否并发,通过获取类名来判断是否是并发类(是并发,则类上用注解 @DisallowConcurrentExecution 来禁止并发)
  2. 然后将其添加到job对象中 JobBuilder.newJob(jobClass) ,再获取job的key(包括名称和分组) JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup)
  3. 再创建cron表达式调度构建器和错误策略
  4. 创建trigger,表示示例创建好了
  5. 放入参数 jobDetail.getJobDataMap()
  6. 调用API判断,如果数据库已存在,先删除
  7. 调用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() 方法,下面有两个子类,分别是 QuartzDisallowConcurrentExecutionQuartzJobExecution ,内容都是一样的,我们随便点进一个查看,可以看到有 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);
}

posted @ 2020-12-07 10:59  刘条条  阅读(9182)  评论(0编辑  收藏  举报