开发者社区> yemon> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

Spring 整合Quartz 2实现定时任务四:细化调整及一些已知的问题

简介:
+关注继续查看

之前已经把功能基本都实现了,这里我们再来优化一下代码。

我们发现,在创建、修改、和删除定时任务时,对于quartz的操作其实是可以封装成一个简单的工具辅助类的,如创建的代码可以抽取成:

/**
 * 创建定时任务
 *
 * @param scheduler the scheduler
 * @param jobName the job name
 * @param jobGroup the job group
 * @param cronExpression the cron expression
 * @param isSync the is sync
 * @param param the param
 */
public static void createScheduleJob(Scheduler scheduler, String jobName, String jobGroup,
                                     String cronExpression, boolean isSync, Object param) {
    //同步或异步
    Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class;
    //构建job信息
    JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build();
    //放入参数,运行时的方法可以获取
    jobDetail.getJobDataMap().put(ScheduleJobVo.JOB_PARAM_KEY, param);
    //表达式调度构建器
    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
    //按新的cronExpression表达式构建一个新的trigger
    CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup)
        .withSchedule(scheduleBuilder).build();
    try {
        scheduler.scheduleJob(jobDetail, trigger);
    } catch (SchedulerException e) {
        LOG.error("创建定时任务失败", e);
        throw new ScheduleException("创建定时任务失败");
    }
}

把任务的具体信息包括Scheduler都使用参数方式传入。

看过前面文章的同学或许还记得,quartz在spring中需要声明的对象只剩下一行:

<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" />

既然这个schedulerFactoryBean只是在spring中声明一下,并没有做特殊的操作,在辅助的工具类中直接使用单例模式创建一个不是更好,还能少传一个参数?

你想的没错,这种方式的确更好还能解耦,但是我们来看一下SchedulerFactoryBean类的代码:

public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean<Scheduler>, BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle {
//......
}

它实现了spring的FactoryBean接口,也就是说它在创建时并不是简单的new而已,还夹杂了一些其它的复杂行为,所以我们也没必要特地的去怎么怎么样,还是在spring中声明一下吧。

另外,我们看它的getObject()方法:

public Scheduler getObject() {
	return this.scheduler;
}

发现它实际返回的已经是Scheduler对象,既然如此在我们类中就不必注入schedulerFactoryBean再调用getScheduler()这么麻烦了,可以直接声明Scheduler对象:
@Service
public class ScheduleJobServiceImpl implements ScheduleJobService {
    /** 调度Bean */
    @Autowired
    private Scheduler scheduler;
	//......
}

当然你注入schedulerFactoryBean也不会有错,看过spring源码的同学应该立马就能明白这是getBean("bean")和getBean("&bean")的区别了。

另外说说前面漏掉的两个地方。

一、更新任务

先前我们在更新任务时,虽然更新了定时任务的执行时间,但是并没有对参数进行更新,即使用context.getMergedJobDataMap().get(...)方法获取到的参数还是旧的。

假设我们更新了任务的时间表达式,任务已按新的时间表达式在执行,但在获取到参数后发现时间表达式还是原来的。

尝试对参数进行更新,使用如下代码:

JobDetail jobDetail = scheduler.getJobDetail(getJobKey(jobName, jobGroup));
//jobDetail = jobDetail.getJobBuilder().ofType(jobClass).build();
//更新参数 实际测试中发现无法更新
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.put(ScheduleJobVo.JOB_PARAM_KEY, param);
jobDetail.getJobBuilder().usingJobData(jobDataMap);

发现无法更新,试过其它几个api发现都不行,没有办法,最后采用了先删除任务再进行创建的方式来迂回实现参数的更新。demo中更新任务有直接修改方式和删除修改方式,区别就在这里。

二、任务的同步和异步

同步和异步在quartz 2.2的版本中对于使用者来说区别只在于是否在job类上添加了@DisallowConcurrentExecution注解。

按时这个特点我们建立两个job的实现工厂类,在其中一个类上添加注解@DisallowConcurrentExecution,然后可以根据添加任务时的参数来确定具体使用哪个:

//同步或异步
Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class;
需要注意在定时任务运行时更新是没有办法改变同步和异步的。

接下来说说我在整合使用时碰到的一些已知问题。

一、更新任务时参数问题。也就是前面说的无法更新任务中传入的参数。

二、同步或异步在定时任务运行时修改是不能改变的,这个在前面也提到了。

三、在定时任务运行时修改,可能会该让任务长时间处于线程阻塞状态,即BLOCKED状态,即使你的任务中只有简单的一行System.out输出。要使它恢复也很简单,删除重建即可。

四、定时任务运行两次的问题。这个也是网上传的最多的问题,这里来着重的说一下。

网上流传引起该问题的原因目前主要有两个说法:

1 spring配置文件加载了多次,导致quartz的bean被实例化多次而导致任务多次执行。

2 tomcat的webapps目录问题。tomcat运行时加载了两次配置文件导致任务多次执行。

这两个说法在我的demo中应该并不存在,但为了验证我也尝试了下。

不使用tomcat,在main方法中用编程的方式启动spring,甚至不使用spring,直接用quartz官方给出的代码:

try {
    // Grab the Scheduler instance from the Factory 
    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    // and start it off
    scheduler.start();
    scheduler.shutdown();
} catch (SchedulerException se) {
    se.printStackTrace();
}

问题还是存在,这就说明不是配置文件加载的问题了,这应该是quartz本身存在的一个bug,而且这个多次运行是很有规律的,基本按如下套路走:

  • 定为5秒运行一次,一切正常,没有多次执行现象发生。
  • 定为10秒运行一次,一切正常,没有多次执行现象发生。
  • 定为29秒运行一次,运行时一次正常,一次不正常。
  • 定为59秒运行一次,运行时一次正常,一次不正常。

以上是我实测得出的,再长时间就没测了,毕竟太耗时。在有运行两次的现象时都是间隔的,即一次正常一次不正常这种方式。

既然推断是quartz本身存在bug,那我们又要如何解决这个问题了?

其实在我个人看来,这个问题是无关紧要的,为什么说无关紧要呢?这就涉及到你项目业务设计的是否完善,代码是否健壮了。

一个设计良好的业务方法,特别是那些供外部调用的接口或方法,应该都支持幂等性,何为幂等性?即这个方法同样的参数至少在一个时间区间内,我调用1次和调用10次100次,结果都是一样的。

支持了幂等性,前面说的运行两次的情况是不是就无关紧要了?在有些定时任务为分布式设计的系统(后面会探讨)中,为了确保定时任务的执行甚至会故意人为的去调用两次。

当然支持幂等性最好是在进入方法时就判断,发现已经执行过时就立即返回而不是真的再去同样的结果再执行一遍,以节省资源。





版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Spring Boot 应用可视化监控,一目了然!
Spring Boot 应用可视化监控,一目了然!
184 0
Spring Boot中使用Spring Security进行安全控制
Spring Boot中使用Spring Security进行安全控制
150 0
Spring Boot 实现定时任务的 4 种方式
定时任务实现的几种方式: Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
112 0
Spring Boot 2.3.0 发布
Spring Boot 2.3.0 发布
59 0
为什么 Spring Boot 2.3.0 放弃 Maven 转投 Gradle ?
为什么 Spring Boot 2.3.0 放弃 Maven 转投 Gradle ?
59 0
企业级 SpringBoot 教程 (二)Spring Boot配置文件详解
springboot采纳了建立生产就绪Spring应用程序的观点。 Spring Boot优先于配置的惯例,旨在让您尽快启动和运行。在一般情况下,我们不需要做太多的配置就能够让spring boot正常运行。
1138 0
Java 程序员必会的技术——Spring boot
每一位程序员都有一个英雄梦,幻想着有朝一日能够 拳打国内BAT,脚踢硅谷FLG。至少至少,也要成为后厂村一霸。 对于后端程序员来说,有一项至关重要的技术可以帮助我们早日实现梦想。
1566 0
Spring boot 通用配置文件模板
001 # =================================================================== 002 # COMMON SPRING BOOT PROPERTIES 003 # 004 # This sample file is provided as a guideline.
1610 0
Spring Boot 启动加载数据 CommandLineRunner
实际应用中,我们会有在项目服务启动的时候就去加载一些数据或做一些事情这样的需求。 为了解决这样的问题,Spring Boot 为我们提供了一个方法,通过实现接口 CommandLineRunner 来实现。
966 0
+关注
65
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载