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依赖;




  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.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) 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) 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.
     * 

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注解开启使用,指定spring代理模式为cglib而不是jdk动态代理,避免代理Job类注入失败;

五、开源项目

https://github.com/caojiantao/peppa

你可能感兴趣的:(Java Web定时任务这一篇就够了)