一、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
共同学习,写下你的评论
评论加载中...
作者其他优质文章