一、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依赖;
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjt.demo</groupId>
<artifactId>quartz</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>quartz</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.compile.jdk>1.8</project.compile.jdk>
<maven-compiler-plugin.version>2.3.2</maven-compiler-plugin.version>
<spring.version>4.3.10.RELEASE</spring.version>
<quartz.version>2.2.1</quartz.version>
</properties>
<dependencies>
<!-- springmvc所需jar包(依赖了spring核心jar) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring3.2以后的貌似都要加上这个jar依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 动态定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
</dependencies>
<build>
<finalName>quartz</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${project.compile.jdk}</source>
<target>${project.compile.jdk}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build></project>2、测试Job类
package com.cjt.demo;public class TestJob { /**
* 定时任务具体执行方法
*/
public void execute() {
System.out.println("测试定时任务执行...");
}
}3、spring配置文件
<?xml version="1.0" encoding="UTF-8"?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- 1、定义定时任务bean --> <bean id="testJob" class="com.cjt.demo.TestJob"/> <!-- 2、定义定时任务执行详情detail(关联定时任务bean,和具体执行方法method) --> <bean id="testJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="testJob"/> <property name="targetMethod" value="execute"/> </bean> <!-- 3、定义定时任务触发器trigger(触发条件) --> <bean id="testJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="testJobDetail"/> <property name="cronExpression" value="0/5 * * * * ?"/> </bean> <!-- 注入Scheduler --> <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref local="testJobTrigger"/> </list> </property> </bean></beans>
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
*/@Componentpublic 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的一些属性,那么在执行的时候,是怎么定位到执行的定时任务实例呢?
// 新建一个jobJobDetail 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.
* <p>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
*/@Componentpublic 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@Componentpublic 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注解开启使用
<aop:aspectj-autoproxy proxy-target-class="true"/>,指定spring代理模式为cglib而不是jdk动态代理,避免代理Job类注入失败;
五、开源项目
https://github.com/caojiantao/peppa
作者:叫我宫城大人
链接:https://www.jianshu.com/p/bc1e27711216