一、Java定时任务
1、Timer
java.util
包下面一个工具类,从1.3开始便支持了;
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello world");
}
}, 0, 1000);
说明下后两个参数分别是delay延迟执行,和period执行间隔,单位都是毫秒。
2、ScheduledExecutorService
java.util.concurrent
包下面,从1.5开始支持;
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(() -> System.out.println("hello world"), 0, 1, TimeUnit.SECONDS);
利用定时任务线程池比Timer方式更为合适,Timer执行多任务task时,只要其中某一个任务发生异常导致其他任务也会结束,ScheduledExecutorService则没有这个问题。
二、Spring集成Quartz
敲黑板,Web定时任务;
1、maven依赖;
4.0.0
com.cjt.demo
quartz
1.0-SNAPSHOT
war
quartz
UTF-8
1.8
2.3.2
4.3.10.RELEASE
2.2.1
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-context-support
${spring.version}
org.springframework
spring-jdbc
${spring.version}
org.quartz-scheduler
quartz
${quartz.version}
quartz
org.apache.maven.plugins
maven-compiler-plugin
${maven-compiler-plugin.version}
${project.compile.jdk}
${project.build.sourceEncoding}
2、测试Job类
package com.cjt.demo;
public class TestJob {
/**
* 定时任务具体执行方法
*/
public void execute() {
System.out.println("测试定时任务执行...");
}
}
3、spring配置文件
4、测试程序Main
package com.cjt.demo;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TestJob {
/**
* 定时任务具体执行方法
*/
public void execute() {
System.out.println(DateTimeFormatter.ISO_TIME.format(LocalDateTime.now()) + ":测试定时任务执行...");
}
}
简单加载下spring-quartz.xml
配置文件测试即可,根据上面触发器的cronExpression
每5秒执行定时任务,运行程序:
18:10:20.183:测试定时任务执行...
18:10:25.003:测试定时任务执行...
18:10:30.023:测试定时任务执行...
18:10:35.001:测试定时任务执行...
18:10:40.002:测试定时任务执行...
18:10:45.007:测试定时任务执行...
三、动态定时任务
此处不要写死,将来必有大改。
1、创建定时任务表,及实体类
CREATE TABLE `quartz` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`group` varchar(255) DEFAULT NULL,
`status` tinyint(1) DEFAULT '0',
`cron_expre` varchar(255) DEFAULT NULL,
`desc` varchar(255) DEFAULT NULL,
`job_class` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
package com.cjt.demo;
/**
* 定时计划基本信息
*
* @author caojiantao
*/
public class Quartz {
/**
* 任务id
*/
private Integer id;
/**
* 任务名称
*/
private String name;
/**
* 任务分组
*/
private String group;
/**
* 任务状态
*/
private Boolean status;
/**
* 任务运行时间表达式
*/
private String cronExpre;
/**
* 任务描述
*/
private String desc;
private String jobClass;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getCronExpre() {
return cronExpre;
}
public void setCronExpre(String cronExpre) {
this.cronExpre = cronExpre;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
public String getJobClass() {
return jobClass;
}
public void setJobClass(String jobClass) {
this.jobClass = jobClass;
}
}
2、创建定时任务管理类
因为spring是依据全局scheduler来管理定时任务的,所以我们要注入这个bean倒管理类中;
package com.cjt.demo;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationTargetException;
/**
* @author caojiantao
*/
@Component
public class QuartzJobManager {
private final Scheduler scheduler;
private final ApplicationContext context;
@Autowired
public QuartzJobManager(Scheduler scheduler, ApplicationContext context) {
this.scheduler = scheduler;
this.context = context;
}
/**
* 添加定时任务
*/
@SuppressWarnings("unchecked")
public void addJob(Quartz job) {
// 根据name和group获取trigger key,判断是否已经存在该trigger
TriggerKey triggerKey = TriggerKey.triggerKey(job.getName(), job.getGroup());
try {
Trigger trigger = scheduler.getTrigger(triggerKey);
if (trigger == null) {
// 新建一个job
JobDetail jobDetail = JobBuilder.newJob((Class extends Job>) Class.forName(job.getJobClass()))
.withIdentity(job.getName(), job.getGroup())
.withDescription(job.getDesc())
.build();
// 新建一个trigger
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpre())
// 定时任务错过处理策略,避免resume时再次执行trigger
.withMisfireHandlingInstructionDoNothing();
trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(scheduleBuilder)
.build();
// scheduler设置job和trigger
scheduler.scheduleJob(jobDetail, trigger);
} else {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpre())
.withMisfireHandlingInstructionDoNothing();
TriggerBuilder builder = trigger.getTriggerBuilder().withIdentity(triggerKey);
trigger = builder.withSchedule(scheduleBuilder).build();
// 根据trigger key重新设置trigger
scheduler.rescheduleJob(triggerKey, trigger);
}
// job状态暂停
if (!job.getStatus()) {
pauseJob(job);
}
} catch (SchedulerException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 暂停定时任务
*/
public void pauseJob(Quartz job) {
try {
scheduler.pauseTrigger(TriggerKey.triggerKey(job.getName(), job.getGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 继续定时任务
*/
public void resumeJob(Quartz job) {
try {
scheduler.resumeTrigger(TriggerKey.triggerKey(job.getName(), job.getGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 移除定时任务
*/
public void removeJob(Quartz job) {
try {
scheduler.pauseTrigger(TriggerKey.triggerKey(job.getName(), job.getGroup()));
scheduler.unscheduleJob(TriggerKey.triggerKey(job.getName(), job.getGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 执行定时任务
*/
public boolean executeJob(String clazz) {
try {
Class> jobClass = Class.forName(clazz);
Object job = context.getBean(jobClass);
jobClass.getDeclaredMethod("execute", JobExecutionContext.class).invoke(job, (Object) null);
return true;
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
return false;
}
}
}
这里有三点特别说明下:
- 定时任务根据Trigger Key来确定唯一性;
- 暂停期间的定时任务处理策略可以
withMisfireHandlingInstructionDoNothing()
避免多次执行; - 个人将定时任务实体注入到spring容器中,手动执行定时任务时直接从容器中取而不用
newInstance()
;
3、定时任务注入service
在动态添加定时任务时,只是传入了job的一些属性,那么在执行的时候,是怎么定位到执行的定时任务实例呢?
// 新建一个job
JobDetail jobDetail = JobBuilder.newJob((Class extends Job>) Class.forName(job.getJobClass()))
.withIdentity(job.getName(), job.getGroup())
.withDescription(job.getDesc())
.build();
在之前debug过程中,发现定时任务的真正执行在org.quartz.core
的initialize
方法中:
public void initialize(QuartzScheduler sched)
throws SchedulerException {
...
Job job = sched.getJobFactory().newJob(firedTriggerBundle, scheduler);
...
}
进一步查看scheduler的JobFactory
中的newJob
方法:
public class AdaptableJobFactory implements JobFactory {
...
@Override
public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
try {
Object jobObject = createJobInstance(bundle);
return adaptJob(jobObject);
}
catch (Exception ex) {
throw new SchedulerException("Job instantiation failed", ex);
}
}
/**
* Create an instance of the specified job class.
* Can be overridden to post-process the job instance.
* @param bundle the TriggerFiredBundle from which the JobDetail
* and other info relating to the trigger firing can be obtained
* @return the job instance
* @throws Exception if job instantiation failed
*/
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
return bundle.getJobDetail().getJobClass().newInstance();
}
...
}
一目了然,通过class的反射当然不能使用我们自己注入的定时任务bean,也就注入不了service,那么目标很明确,通过重写JobFactory
的createJobInstance()
方法:
package com.cjt.quartz;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* @author caojiantao
* @since 2018-02-13 19:59:48
*/
@Component
public class JobFactory extends AdaptableJobFactory {
private final ApplicationContext context;
@Autowired
public JobFactory(ApplicationContext context) {
this.context = context;
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) {
return context.getBean(bundle.getJobDetail().getJobClass());
}
}
通过改写createJobInstance
指定执行的定时任务实例是我们注入的bean,解决定时任务Job不能注入service的问题。
四、集群部署
可能上面的程序已经很满意了,但是放在集群中,每台服务器都会跑这些定时任务,导致执行多次造成未知问题。
个人有几个解决方案:
- 指定定时任务服务器IP地址;(最简单最捞)
- 采用quartz集群部署方案;(繁杂但高效)
- 新建任务执行记录表,通过唯一性索引约束加锁,job执行aop切面处理执行判定;(有点意思)
quartz集群方案需要增加十几张数据表!个人表示不想,下面说说第三种方案。
1、创建定时任务执行表
CREATE TABLE `quartz_execute` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_class` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_job_class` (`job_class`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
注意给job_class
添加唯一性索引,然后在同一时间只有一台服务器能插入定时任务成功,而达到我们的目的。
2、创建切面类,统一判定处理
package com.cjt.quartz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 定时任务切面,用作解决集群部署任务单点执行
*
* @author caojiantao
*/
@Aspect
@Component
public class JobAspect {
private final IQuartzExecuteService executeService;
@Autowired
public JobAspect(IQuartzExecuteService executeService) {
this.executeService = executeService;
}
@Around("execution(* com.cjt.quartz.job..*.execute(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
Object context = joinPoint.getArgs()[0];
if (context == null) {
// 执行上下文为空代表手动执行
result = joinPoint.proceed();
return result;
}
String jobClass = ((JobExecutionContext) context).getJobDetail().getJobClass().getName();
if (executeService.saveExecute(jobClass)) {
result = joinPoint.proceed();
executeService.removeExecuteByJobClass(jobClass);
}
return result;
}
}
注意以下四点:
- 切面规则定义好,后面Job集中放在这个规则下面;
- 参数需要非空校验,因为手动执行没有
JobExecutionContext
; - 执行定时任务一定要记得remove释放;
- aop注解开启使用
,指定spring代理模式为cglib而不是jdk动态代理,避免代理Job类注入失败;
五、开源项目
https://github.com/caojiantao/peppa