为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Java

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

这里有三点特别说明下:

  1. 定时任务根据Trigger Key来确定唯一性;

  2. 暂停期间的定时任务处理策略可以withMisfireHandlingInstructionDoNothing()避免多次执行;

  3. 个人将定时任务实体注入到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.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
 */@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的问题。

四、集群部署

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

个人有几个解决方案:

  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@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;
    }
}

注意以下四点:

  1. 切面规则定义好,后面Job集中放在这个规则下面;

  2. 参数需要非空校验,因为手动执行没有JobExecutionContext

  3. 执行定时任务一定要记得remove释放;

  4. aop注解开启使用<aop:aspectj-autoproxy proxy-target-class="true"/>,指定spring代理模式为cglib而不是jdk动态代理,避免代理Job类注入失败;

五、开源项目

https://github.com/caojiantao/peppa

作者:叫我宫城大人
链接:https://www.jianshu.com/p/bc1e27711216

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消