本文已录入至我的Github库房DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star
⭐⭐⭐⭐⭐
转载请注明出处:
blog.csdn.net/weixin_4346…
前语
最近学习了守时使命相关的内容,了解了下Quartz结构,可是原生的一些API用起来不太便利,所以依照我自己的运用场景做了一些封装。这篇文章就带小伙伴们了解一下Quartz的根本运用。包括根本概念以及怎样动态地对守时使命进行CRUD,而且怎样完成守时使命的耐久化以及使命康复。
一、Quartz的根本运用
Quartz 是一个开源的作业调度结构。在运用这个结构之前,咱们需求知道几个根本的概念Job,Trigger以及Schedule:
Job和JobDetail
既然是作业调度,那么必定要有作业呀,这个作业便是Job。在界说咱们自己的Job的时分,只需求完成Job接口,然后在execute办法里编写详细的事务逻辑即可。也能够承继QuartzJobBean类并重写executeInternal办法,因为QuartzJobBean类也是完成了Job接口。
public class TemplateJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 欢迎重视我的微信大众号:Robod
}
}
JobDetail为Job实例提供了许多设置特点,以及JobDataMap成员变量特点,它用来存储特定Job实例的状况信息,调度器需求凭借Jobdetail对象来增加Job实例。
Trigger
有了使命之后,就该设置使命的触发事件了,在Quartz中运用Trigger来描绘触发Job履行的时刻触发规矩。一个Job能够增加多个Trigger,可是一个Trigger只能绑定一个Job。
Quartz中一共有四种Trigger:
-
SimpleTrigger
:这种触发器能够在给守时刻触发作业,而且可选择以指定的时刻距离重复。 -
CronTrigger
:用过守时使命的小伙伴应该会猜到这个是干什么的吧。这个触发器能够设置一个Cron表达式,指定使命的履行周期。 -
CalendarIntervalTrigger
:用于根据重复的日历时刻距离触发。 -
DailyTimeIntervalTrigger
:用于根据每天重复的时刻距离触发使命的触发器。
后面两种我并没有用过,所以下文的介绍首要基于前两种。后两种感兴趣的小伙伴能够自行研讨。
Schedule
Schedule所扮演的是一个履行者的人物,将JobDetail和Trigger注册到Schedule中,它就会依照指定的规矩去履行使命。
界说一个守时使命
假如是Springboot的项目,增加如下依靠即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
界说使命分为三步:界说一个JobDetail、界说一个Trigger、运用Scheduler去履行使命。
JobDetail jobDetail = JobBuilder.newJob(TemplateJob.class).withIdentity("使命名", "使命组名").build();
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("触发器名", "触发器组名")
.startNow()
.build();
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
上面这段代码中,将触发器设为startNow,也便是当即履行。也能够运用startAt以及endAt办法设置开端时刻以及完毕时刻等。
关于SimpleTrigger的详细用法能够参阅这个链接:xuzongbao.gitbooks.io/quartz/cont…
在Job中获取自界说参数
在实践的运用过程中,咱们或许需求在创立一个Job时指定一些参数用于详细的事务场景,就能够凭借JobDataMap。比方指定一个时刻给某个人发送奖品,那么在创立使命时就需求用户id,奖品称号,奖品id等信息。咱们就能够在界说JobDetail时,运用usingJobData办法设置一些参数,或许运用setJobData办法将界说好的jobDetail填入进去。
JobDetail jobDetail = JobBuilder.newJob(TemplateJob.class)
.usingJobData("userId", "VIP2345678")
.usingJobData("awardName", "100元优惠券")
.usingJobData("awardId", "YHQ675567687765")
.usingJobData("awardValue",6.6)
.withIdentity("使命名", "使命组名")
.build();
//-----------或许---------------
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("userId", "VIP5465756453");
jobDataMap.put("awardName", "100元优惠券");
jobDataMap.put("awardId", "YHQ67354747443");
jobDataMap.put("awardValue",6.6);
JobDetail jobDetail2 = JobBuilder.newJob(TemplateJob.class)
.setJobData(jobDataMap)
.withIdentity("使命名", "使命组名")
.build();
JobDataMap就能够将它理解为HashMap,用法都是类似的。然后在Job中就能够获取到JobDataMap,然后拿到相应的参数:
public class AwardJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String userId = jobDataMap.getString("userId");
String awardName = jobDataMap.getString("awardName");
String awardId = jobDataMap.getString("awardId");
double awardValue = jobDataMap.getDouble("awardValue");
//…………发奖品…………
}
}
二、Quartz东西类完成守时使命的动态CRUD
参阅:blog.csdn.net/u010377605/…
上一节中说到界说一个守时使命要分为三步,可是在一个项目中必然会在多处都用到守时使命,假如每次界说都要写这样一段代码明显不行优雅。为了便利运用,能够写一个通用的东西类去完成守时使命的CRUD。
每个Job的jobName都必须是仅有的,建议运用和事务相关的主键id作为jobName。这样即能够保证仅有性,也能够经过jobName判别这个某个守时使命详细是做什么操作的,还能够在Job的execute办法中获取到这个主键id而履行相关操作,就不必额定传递参数了。比方订票体系中在发车前给用户发短信,就能够将订单的id作为jobName。
JobDetail和Trigger还需求指定一个分组,一个项目中运用到守时使命的分组应该是固定数量的,界说一个枚举类将需求用到的分组称号放在里边,在不同的事务场景下创立守时使命时选用不同的分组即可,需求额定的再往里边增加就行。
@Getter
@AllArgsConstructor
public enum QuartzGroupEnum {
T1( "测验分组1"),
T2("测验分组2");
private final String value;
}
增加一个守时使命
@Component
public class QuartzUtil {
private static final SchedulerFactory SCHEDULER_FACTORY = new StdSchedulerFactory();
@Autowired
private QuartzService quartzService;
/**
* 增加一个守时使命
*
* @param name 使命名。每个使命仅有,不能重复。便利起见,触发器名也设为这个
* @param group 使命分组。便利起见,触发器分组也设为这个
* @param jobClass 使命的类类型 eg:TemplateJob.class
* @param startTime 使命开端时刻。传null便是当即开端
* @param endTime 使命完毕时刻。假如是一次性使命或永久履行的使命就传null
* @param cron 时刻设置表达式。传null便是一次性使命
*/
public boolean addJob(String name, String group, Class<? extends Job> jobClass,
LocalDateTime startTime, LocalDateTime endTime, String cron, JobDataMap jobDataMap) {
try {
// 第一步: 界说一个JobDetail
JobDetail jobDetail = JobBuilder.newJob(jobClass).
withIdentity(name, group).setJobData(jobDataMap).build();
// 第二步: 设置触发器
TriggerBuilder<Trigger> triggerBuilder = newTrigger();
triggerBuilder.withIdentity(name, group);
triggerBuilder.startAt(toStartDate(startTime));
triggerBuilder.endAt(toEndDate(endTime)); //设为null则表明不会中止
if (StrUtil.isNotEmpty(cron)) {
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
}
Trigger trigger = triggerBuilder.build();
//第三步:调度器设置
Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
private static Date toEndDate(LocalDateTime endDateTime) {
// 完毕时刻能够为null,所以endDateTime为null,直接回来null即可
return endDateTime != null ?
DateUtil.date(endDateTime) : null;
}
private static Date toStartDate(LocalDateTime startDateTime) {
// startDateTime为空时回来当时时刻,表明当即开端
return startDateTime != null ?
DateUtil.date(startDateTime) : new Date();
}
}
在需求增加守时使命的地方,咱们只需求调用这个办法,将指定的几个参数传入进去,就能够界说并开端一个守时使命了。现在咱们只需求重视怎样在Job中编写自己的事务代码而不需求去关怀怎样创立一个守时使命了。
在界说触发器时,假如startTime参数传过来为null的话,就表明是当即履行,那么就在startAt中将现在的时刻传入。不必判别是应该用startAt仍是startNow,因为从源码中能够看到,startNow办法也是将时刻设为现在。
这儿有一个地方需求留意,在界说触发器时,写的是Trigger而不是SimpleTrigger或许CronTrigger。这是因为我想用这个办法去增加一个一次性使命或许周期性使命,这样写的话,因为Java多态的特点,假如不指定cron,在运行时便是自动转型为SimpleTrigger,指定了cron后,运行时就会自动转型为CronTrigger。这样咱们就不必关怀是该用SimpleTrigger仍是CronTrigger了。这一点需求留意,因为在下面的小节中会用到这个常识点。
触发器设置时刻用的是Date,可是我平时用LocalDateTime比较多,所以在传参时我选择运用LocalDateTime然后调用一个办法进行转化。
修正一个守时使命
/**
* 修正一个使命的开端时刻、完毕时刻、cron。不改的就传null
*
* @param name 使命名。每个使命仅有,不能重复。便利起见,触发器名也设为这个
* @param group 使命分组。便利起见,触发器分组也设为这个
* @param newStartTime 新的开端时刻
* @param newEndTime 新的完毕时刻
* @param cron 新的时刻表达式
*/
public boolean modifyJobTime(String name, String group, LocalDateTime newStartTime,
LocalDateTime newEndTime, String cron) {
try {
Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(name, group);
Trigger oldTrigger = scheduler.getTrigger(triggerKey);
if (oldTrigger == null) {
return false;
}
TriggerBuilder<Trigger> triggerBuilder = newTrigger();
triggerBuilder.withIdentity(name, group);
if (newStartTime != null) {
triggerBuilder.startAt(toStartDate(newStartTime)); // 使命开端时刻设定
} else if (oldTrigger.getStartTime() != null) {
triggerBuilder.startAt(oldTrigger.getStartTime()); //没有传入新的开端时刻就不变
}
if (newEndTime != null) {
triggerBuilder.endAt(toEndDate(newEndTime)); // 使命完毕时刻设定
} else if (oldTrigger.getEndTime() != null) {
triggerBuilder.endAt(oldTrigger.getEndTime()); //没有传入新的完毕时刻就不变
}
if (StrUtil.isNotEmpty(cron)) {
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
} else if (oldTrigger instanceof CronTrigger) {
String oldCron = ((CronTrigger) oldTrigger).getCronExpression();
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(oldCron));
}
Trigger newTrigger = triggerBuilder.build();
scheduler.rescheduleJob(triggerKey, newTrigger); // 修正触发时刻
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
修正使命其实便是重新设置一个Trigger。先经过触发器名和触发器组名(也是使命名和使命组名)将旧的触发器 oldTrigger 查询出来,因为咱们会用到其间的一些信息,然后界说一个新的触发器,对于不需求修正的参数就继续运用 oldTrigger 中的。
这儿有段代码留意一下:
if (StrUtil.isNotEmpty(cron)) {
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
} else if (oldTrigger instanceof CronTrigger) {
String oldCron = ((CronTrigger) oldTrigger).getCronExpression();
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(oldCron));
}
前面提过,设置了Cron便是CronTrigger,未设置便是SimpleTrigger。所以这儿就能够经过Trigger的类型来判别是哪一种,传过来的cron为null表明不需求修正,假如之前是一次性使命就不必管,假如之前便是周期性使命,那么必定是CronTrigger,在不需求修正的状况下,就将cron设为之前的。
撤销一个守时使命
public boolean cancelJob(String jobName, String groupName) {
try {
Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, groupName);
scheduler.pauseTrigger(triggerKey); // 中止触发器
scheduler.unscheduleJob(triggerKey); // 移除触发器
scheduler.deleteJob(JobKey.jobKey(jobName, groupName)); // 删去使命
} catch (Exception e) {
e.printStackTrace();
return false;
}
//将数据库中的使命状况设为 撤销
return true;
}
撤销就比较简略了,直接将触发器中止并移除,最终删去使命即可。
查询一切的守时使命
public List<QuartzEntity> getAllJobs() throws SchedulerException {
Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
List<QuartzEntity> quartzJobs = new ArrayList<>();
try {
List<String> triggerGroupNames = scheduler.getTriggerGroupNames();
for (String groupName : triggerGroupNames) {
GroupMatcher<TriggerKey> groupMatcher = GroupMatcher.groupEquals(groupName);
Set<TriggerKey> triggerKeySet = scheduler.getTriggerKeys(groupMatcher);
for (TriggerKey triggerKey : triggerKeySet) {
Trigger trigger = scheduler.getTrigger(triggerKey);
JobKey jobKey = trigger.getJobKey();
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
//组装数据
QuartzEntity entity = new QuartzEntity();
entity.setJobName(jobDetail.getKey().getName());
entity.setGroupName(jobDetail.getKey().getGroup());
entity.setStartTime(LocalDateTimeUtil.of(trigger.getStartTime()));
entity.setEndTime(LocalDateTimeUtil.of(trigger.getStartTime()));
entity.setJobClass(jobDetail.getJobClass().getName());
if (trigger instanceof CronTrigger) {
entity.setCron(((CronTrigger) trigger).getCronExpression());
}
entity.setJobDataMapJson(JSONUtil.toJsonStr(jobDetail.getJobDataMap()));
quartzJobs.add(entity);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return quartzJobs;
}
先获取一切的触发器组名,再遍历获取每个触发器组中的触发器Set调集,最终遍历触发器Set调集获取JobDetail信息,然后用一个QuartzEntity对象对数据进行封装回来,再将entity放入List中。
三、在Job中注入Bean
在履行详细的守时使命时,必定会用到相应的Service,可是经过@Autowired或许构造器注入的办法都会注入失败。能够经过一个东西类去完成在Job中注入Bean。在Service的完成类上一定要增加@Service(“xxxxService”)注解,否则会注入失败。
@Component
public class SpringContextJobUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
@SuppressWarnings("static-access")
public void setApplicationContext(ApplicationContext context)
throws BeansException {
this.context = context;
}
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
调用getBean办法就能够正常注入了。
public class TemplateJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
QuartzService quartzService = (QuartzService) SpringContextJobUtil.getBean("quartzService");
//…………欢迎重视我的微信大众号:Robod
}
}
参阅这篇文章:www.jianshu.com/p/aff9199b4…
四、耐久化Job并完成程序发动时使命康复
当遇到更新版本等状况时,必定要将程序给停了,可是程序中止后那些还未开端或许没履行完的守时使命就没了。所以咱们需求将使命耐久化到数据库中,然后在程序发动时将这些使命进行康复。
在数据库中增加一张表,用于存储Job的信息。
然后在QuartzUtil中界说一个 recoveryAllJob 办法用于康复守时使命:
public void recoveryAllJob() {
List<QuartzEntity> tasks = quartzService.notStartOrNotEndJobs();
if (tasks != null && tasks.size() > 0) {
for (QuartzEntity task : tasks) {
try {
JobDataMap jobDataMap = JSONUtil.toBean(task.getJobDataMapJson(), JobDataMap.class);
JobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>) Class.forName(task.getJobClass()))
.withIdentity(task.getJobName(), task.getGroupName())
.setJobData(jobDataMap).build();
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
triggerBuilder.withIdentity(task.getJobName(), task.getGroupName());
triggerBuilder.startAt(toStartDate(task.getStartTime()));
triggerBuilder.endAt(toEndDate(task.getEndTime()));
if (StrUtil.isNotEmpty(task.getCron())) {
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(task.getCron()));
}
Trigger trigger = triggerBuilder.build();
Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
首先从数据库中将需求康复的使命查询出来,然后遍历使命将其挨个创立出来。
然后在前面的CRUD办法中增加对数据库的一些操作,也别忘了在一次性使命的Job中履行完成后调用quartzService.modifyTaskStatus(jobName, “1”) 办法将使命的状况修正为已完成,否则程序发动后使命又康复过来了:
@Component
public class QuartzUtil {
public boolean addJob(String name, String group, Class<? extends Job> jobClass,
LocalDateTime startTime, LocalDateTime endTime, String cron, JobDataMap jobDataMap) {
//…………
//存储到数据库中
QuartzEntity entity = new QuartzEntity();
entity.setJobName(name);
entity.setGroupName(group);
entity.setStartTime(startTime != null ? startTime : LocalDateTime.now());
entity.setEndTime(endTime);
entity.setJobClass(jobClass.getName());
entity.setCron(cron);
entity.setJobDataMapJson(JSONUtil.toJsonStr(jobDataMap));
entity.setStatus("0");
quartzService.save(entity);
return true;
}
public boolean modifyJobTime(String name, String group, LocalDateTime newStartTime,
LocalDateTime newEndTime, String cron) {
//…………
// 修正数据库中的记录
QuartzEntity entity = new QuartzEntity();
entity.setJobName(name);
entity.setGroupName(group);
if (newStartTime != null) {
entity.setStartTime(newStartTime);
}
if (newEndTime != null) {
entity.setEndTime(newEndTime);
}
if (StrUtil.isNotEmpty(cron)) {
entity.setCron(cron);
}
return quartzService.modifyJob(entity);
}
public boolean cancelJob(String jobName, String groupName) {
//…………
//将数据库中的使命状况设为 撤销
return quartzService.modifyTaskStatus(jobName, "2");
}
}
在保存和康复使命时,将jobDataMap以Json的办法进行存储。
QuartzService中的代码便是一些根本的CRUD,没有什么好说的,就不在这儿进行阐明了,小伙伴们能够下载完好代码进行查看。(链接在文末)
那么有了康复办法后,怎样在程序发动时调用这个办法呢?
很简略,只需求修正发动类,让其完成ApplicationRunner接口并完成run办法,在run办法中调用康复办法即可。
@SpringBootApplication
@MapperScan("com.robod.quartzdemo.mapper")
public class QuartzDemoApplication implements ApplicationRunner {
@Autowired
private QuartzUtil quartzUtil;
public static void main(String[] args) {
SpringApplication.run(QuartzDemoApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
quartzUtil.recoveryAllJob();
}
}
这样在程序发动时,就会自动地调用recoveryAllJob办法去康复守时使命了。
五、小事例
现在经过一个详细的事例来简略模拟一下该怎样用。假设有这样一个场景:在火车票的订票体系中,在创立订单时设立一个守时使命,在发车前两个小时给乘客发送提醒搭车的短信,用户或许改签或许撤销订单,那么也应该同样的对守时使命进行修正。
先来看一下Service中是怎样运用QuartzUtil来操作守时使命的吧:
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Autowired
private QuartzUtil quartzUtil;
@Override
public String bookTicket(String userId, String ticketId) {
//TODO 查询余票下订单等一些列操作
Order order = new Order();
//…………
//创立一个守时使命
LocalDateTime noticeTime = order.getDepartureTime().minusHours(2); //告诉时刻为发车前两小时
quartzUtil.addJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue(),
DepartureNoticeJob.class, noticeTime, null, null, null);
return "";
}
@Override
public String rebook(Order order) {
//TODO 修正订单等一系列操作
//修正守时使命
LocalDateTime noticeTime = order.getDepartureTime().minusHours(2); //告诉时刻为发车前两小时
quartzUtil.modifyJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue(),
noticeTime, null, null);
return "";
}
@Override
public String cancelOrder(Order order) {
//TODO 撤销订单等一系列操作
//撤销守时使命
quartzUtil.cancelJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue());
return "";
}
}
首先将QuartzUtil给注入进来,然后调用其间相应的办法并将参数传入进去就能够操作守时使命了。这儿的JobName设置为了订单的id,一方面是为了防止重复,另一方面是免去了额定传参的麻烦,因为在Job中只需求用到订单id。假如只有订单id这一个参数不行用,那么再运用JobDataMap 设置自界说的参数也是OK的,详细用法前面也有阐明。GroupName则是在枚举类中界说的。
再来看一下DepartureNoticeJob
中都做了些什么:
public class DepartureNoticeJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
QuartzService quartzService = (QuartzService) SpringContextJobUtil.getBean("quartzService");
OrderService orderService = (OrderService) SpringContextJobUtil.getBean("orderService");
String jobName = context.getJobDetail().getKey().getName();
long orderId = Long.parseLong(jobName);
// TODO 获取订单及用户信息,封装短信内容,调用短信发送模块发送短信
quartzService.modifyTaskStatus(jobName, "1");
}
}
在这个Job中,因为Job的jobName被设为的订单的id,所以咱们能够经过订单的id查询到订单以及用户的相关信息,然后封装短信的内容,进行发送短信操作。因为发短信是一次性使命,那么在完毕后应该修正这条使命的状况为已完毕,否则程序重启后这个使命又被康复了,又会给用户发送重复的信息。
QuartzUtil的运用大约便是这样,用起来仍是非常简略的。
总结
介绍到这儿就完毕了,我感觉这应该足够满意一些根本的事务场景了。小伙伴们能够将代码下载下来进行学习,然后根据自己的需求去做一些更改就能够用在自己的项目中了。代码我没有进行过多的测验,所以或许会有一些瑕疵,不过整体的思路应该是没问题的。
我也在Controller中增加了几个测验接口,并在ApiFox中增加了一些测验用例,将测验用例导入到ApiFox中就能够测验了。
完好的代码以及相关文件:github.com/RobodLee/Qu…
⭐⭐⭐⭐⭐转载请注明出处:blog.csdn.net/weixin_4346…
本文已录入至我的Github库房DayDayUP:github.com/RobodLee/Da…,欢迎Star
假如您觉得文章还不错,请给我来个
点赞
,收藏
,重视
学习更多编程常识,欢迎重视微信大众号『 R o b o d 』: