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

作者: 叫我宫城大人 | 来源:发表于2018-04-16 22:11 被阅读43次

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

    相关文章

      网友评论

        本文标题:Java Web定时任务这一篇就够了

        本文链接:https://www.haomeiwen.com/subject/flehkftx.html