Java Web定时任务这一篇就够了

一、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
 */
@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;
        }
    }
}

这里有三点特别说明下:

  1. 定时任务根据Trigger Key来确定唯一性;
  2. 暂停期间的定时任务处理策略可以withMisfireHandlingInstructionDoNothing()避免多次执行;
  3. 个人将定时任务实体注入到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.coreinitialize方法中:

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,那么目标很明确,通过重写JobFactorycreateJobInstance()方法:

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的问题。

四、集群部署

可能上面的程序已经很满意了,但是放在集群中,每台服务器都会跑这些定时任务,导致执行多次造成未知问题。

个人有几个解决方案:

  1. 指定定时任务服务器IP地址;(最简单最捞)
  2. 采用quartz集群部署方案;(繁杂但高效)
  3. 新建任务执行记录表,通过唯一性索引约束加锁,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;
    }
}

注意以下四点:

  1. 切面规则定义好,后面Job集中放在这个规则下面;
  2. 参数需要非空校验,因为手动执行没有JobExecutionContext
  3. 执行定时任务一定要记得remove释放;
  4. aop注解开启使用<aop:aspectj-autoproxy proxy-target-class="true"/>,指定spring代理模式为cglib而不是jdk动态代理,避免代理Job类注入失败;

五、开源项目

https://github.com/caojiantao/peppa

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容