SpringBoot 定时任务及Quartz定时框架

定时任务创建方法与特点

springboot定时任务主要有以下三种创建方式:

1、基于注解(@Scheduled)

  • 基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
@Configuration      //标记配置类
@EnableScheduling   // 开启定时任务
public class SaticScheduleTask {
    //添加定时任务
    //cron表达式:cron是一个字符串,之间通过空格隔开,分割成6,7个域,每个域代表一种含义
    @Scheduled(cron = "")
    //或直接指定时间间隔,例如:5秒
    //@Scheduled(fixedRate=5000)
    private void configureTasks() {
        System.err.println("定时任务执行时间" + LocalDateTime.now());
    }
}
  • @Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
  • 使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务 。

2、基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。

public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
	taskRegistrar.addTriggerTask(
    //1.添加任务内容(Runnable)
    () -> System.out.println("执行定时任务: " + LocalDateTime.now().toLocalTime()),
    //2.设置执行周期(Trigger)
    triggerContext -> {
        //2.1 从数据库获取执行周期
        String cron = cronMapper.getCron();
        //2.2 合法性校验.
        if (StringUtils.isEmpty(cron)) {
            // Omitted Code ..
        }
        //2.3 返回执行周期(Date)
        return new CronTrigger(cron).nextExecutionTime(triggerContext);
    }
    );
}

3、基于注解设定多线程定时任务

	//@Component注解用于对那些比较中立的类进行注释;
	//相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
	@Component
	@EnableScheduling   // 1.开启定时任务
	@EnableAsync        // 2.开启多线程
	public class MultithreadScheduleTask {

        @Async
        @Scheduled(fixedDelay = 1000)  //间隔1秒
        public void first() throws InterruptedException {
            System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
            System.out.println();
            Thread.sleep(1000 * 10);
        }

        @Async
        @Scheduled(fixedDelay = 2000)
        public void second() {
            System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
            System.out.println();
        }
  	}
  • 在Spring中,基于@Async标注的方法,称之为异步方法;这些方法在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。

定时任务框架: Quartz

什么是Quartz

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:

持久性作业 - 就是保持调度定时的状态;
作业管理 - 对调度作业进行有效的管理;

在我们实际的项目中,当Job过多的时候,肯定不能人工去操作,这时候就需要一个任务调度框架,帮我们自动去执行这些程序。

  1. 首先我们需要定义实现一个定时功能的接口,我们可以称之为Task(或Job)
  2. 有了任务之后,还需要一个能够实现触发任务去执行的触发器,触发器Trigger最基本的功能是指定Job的执行时间,执行间隔,运行次数等。
  3. 有了Job和Trigger后,怎么样将两者结合起来呢?即怎样指定Trigger去执行指定的Job呢?这时需要一个Schedule,来负责这个功能的实现。
    在这里插入图片描述

上面三个部分就是Quartz的基本组成部分:

  • 调度器:Scheduler
  • 任务:JobDetail
  • 触发器:Trigger,包括SimpleTrigger和CronTrigger

Quartz框架使用

  • Scheduler在使用之前需要实例化。一般通过SchedulerFactory来创建一个实例。
  • scheduler实例化后,可以启动(start)、暂停(stand-by)、停止(shutdown)。注意:scheduler被停止后,除非重新实例化,否则不能重新启动;只有当scheduler启动后,即使处于暂停状态也不行,trigger才会被触发(job才会被执行)。
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();

  Scheduler sched = schedFact.getScheduler();

  sched.start();

  // 定义任务并将其绑定到我们的HelloJob类
  JobDetail job = newJob(HelloJob.class)
      .withIdentity("myJob", "group1")
      .build();

  // 触发器定时每四十秒触发
  Trigger trigger = newTrigger()
      .withIdentity("myTrigger", "group1")
      .startNow()
      .withSchedule(simpleSchedule()
          .withIntervalInSeconds(40)
          .repeatForever())
      .build();

  // Tell quartz to schedule the job using our trigger
  sched.scheduleJob(job, trigger);

Quartz框架具体API,Job和Trigger

Quartz API核心接口有:

  • Scheduler – 与scheduler交互的主要API;
  • Job – 你通过scheduler执行任务,你的任务类需要实现的接口;
  • JobDetail – 定义Job的实例;
  • Trigger – 触发Job的执行;
  • JobBuilder – 定义和创建JobDetail实例的接口;
  • TriggerBuilder – 定义和创建Trigger实例的接口;

说明:

  • Scheduler的生命期,从SchedulerFactory创建它时开始,到Scheduler调用shutdown()方法时结束;Scheduler被创建后,可以增加、删除和列举Job和Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。但是,Scheduler只有在调用start()方法后,才会真正地触发trigger(即执行job)

  • SchedulerBuilder接口的各种实现类,可以定义不同类型的调度计划(schedule);

  • DateBuilder类包含很多方法,可以很方便地构造表示不同时间点的java.util.Date实例(如定义下一个小时为偶数的时间点,如果当前时间为9:43:27,则定义的时间为10:00:00)。

Job

  • 一个job就是一个实现了Job接口的类,该接口只有一个方法:
package org.quartz;

public interface Job {
    void execute(JobExecutionContext var1) throws JobExecutionException;
}
  • 当job的一个trigger被触发后,execute()方法会被scheduler的一个工作线程调用;传递给execute()方法的JobExecutionContext对象中保存着该job运行时的一些信息 ,执行job的scheduler的引用,触发job的trigger的引用,JobDetail对象引用,以及一些其它信息。
  • JobDetail对象是在将job加入scheduler时,由客户端程序创建的。它包含job的各种属性设置,以及用于存储job实例状态信息的JobDataMap,例如
	JobDetail jobDetail = newJob(HelloJob.class)
                    .withIdentity("myJob" + myJob.getJobId())
                    .usingJobData("myJob", JSONObject.toJSONString(myJob))
                    .build();

Trigger

  • Trigger用于触发Job的执行。当你准备调度一个job时,你创建一个Trigger的实例,然后设置调度相关的属性。Trigger也有一个相关联的JobDataMap,用于给Job传递一些触发相关的参数。Quartz自带了各种不同类型的Trigger,最常用的主要是SimpleTrigger和CronTrigger。
  • SimpleTrigger主要用于一次性执行的Job(只在某个特定的时间点执行一次),或者Job在特定的时间点执行,重复执行N次,每次执行间隔T个时间单位。CronTrigger在基于日历的调度上非常有用,如“每个星期五的正午”,或者“每月的第十天的上午10:15”等。
  • 为什么既有Job,又有Trigger呢?很多任务调度器并不区分Job和Trigger。有些调度器只是简单地通过一个执行时间和一些job标识符来定义一个Job;其它的一些调度器将Quartz的Job和Trigger对象合二为一。
  • 例如,Job被创建后,可以保存在Scheduler中,与Trigger是独立的,同一个Job可以有多个Trigger;这种松耦合的另一个好处是,当与Scheduler中的Job关联的trigger都过期时,可以配置Job稍后被重新调度,而不用重新定义Job;还有,可以修改或者替换Trigger,而不用重新定义与之关联的Job。

Key

  • 将Job和Trigger注册到Scheduler时,可以为它们设置key,配置其身份属性。Job和Trigger的key(JobKey和TriggerKey)可以用于将Job和Trigger放到不同的分组(group)里,然后基于分组进行操作。同一个分组下的Job或Trigger的名称必须唯一,即一个Job或Trigger的key由名称(name)和分组(group)组成。

JobDetail

 	public class HelloJob implements Job {

        public HelloJob() {
        }

        public void execute(JobExecutionContext context)
          throws JobExecutionException
        {
          System.err.println("Hello!  HelloJob is executing.");
        }
    }
  • 可以看到,我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执行何种类型的job;每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。

  • 那么如何给job实例增加属性或配置呢?如何在job的多次执行中,跟踪job的状态呢?
    答案就是:JobDataMap,JobDetail对象的一部分。

JobDataMap

  • JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。
  • 将job加入到scheduler之前,在构建JobDetail时,可以将数据放入JobDataMap,如下示例:
 	JobDetail jobDetail = newJob(HelloJob.class)
                    .withIdentity("myJob" + myJob.getJobId())
                    .usingJobData("myJob", JSONObject.toJSONString(myJob))
                    //可以写入多个数据
                    .usingJobData("jobSays", "Hello World!")
                    .build();

usingJobData方法

	public JobBuilder usingJobData(String dataKey, String value) {
        this.jobDataMap.put(dataKey, value);
        return this;
    }

在job的执行过程中,可以从JobDataMap中取出数据,如下示例:

public class HelloJob implements Job {

    @Autowired
    private ProjectService projectService;
    @Autowired
    private RegularService regularService;
    @Autowired
    private CompareService compareService;
    @Autowired
    private InterfaceService interfaceService;
    
    @Override
    public void execute(JobExecutionContext context)  {
        //获取trigger
        Trigger trigger = context.getTrigger();
        //获取数据
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String jobJsonString = jobDataMap.getString("myJob");
        //fastJson  解析成具体实体类对象
        cn.XXX.ict.entity.Job job = JSONObject.parseObject(jobJsonString, cn.XXX.ict.entity.Job.class);
        //通过trigger获取job标识
        JobKey jobKey = trigger.getJobKey();

        //主要任务
        ......
       }
}

注意

  • 如果你使用的是持久化的存储机制,在决定JobDataMap中存放什么数据的时候需要小心,因为JobDataMap中存储的对象都会被序列化,因此很可能会导致类的版本不一致的问题;

  • 如果你在job类中,为JobDataMap中存储的数据的key增加set方法那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了。

  • 在Job执行时,JobExecutionContext中的JobDataMap为我们提供了很多的便利。它是JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值。

Job的其它特性
通过JobDetail对象,可以给job实例配置的其它属性有:

  • Durability:如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;
  • RequestsRecovery:如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。

JobExecutionException

  • 最后,是关于Job.execute(…)方法的一些额外细节。execute方法中仅允许抛出一种类型的异常(包括RuntimeExceptions),即JobExecutionException。因此,应该将execute方法中的所有内容都放到一个”try-catch”块中。

本项目中代码实例

线程池配置

<!-- 定时框架线程池配置--!>  
  quartz:
    properties:
      org:
        quartz:
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 50
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

如何为Quartz的Job自动装配Spring容器Bean

Quartz配置

@Configuration
public class QuartzConfig {

    @Autowired
    private MyJobFactory myJobFactory;

    public QuartzConfig(MyJobFactory jobFactory){
        this.myJobFactory = jobFactory;
    }

    /**
     * 配置SchedulerFactoryBean
     * 将一个方法产生为Bean并交给Spring容器管理
     * @return
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        // Spring提供SchedulerFactoryBean为Scheduler提供配置信息,并被Spring容器管理其生命周期
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        // 设置自定义Job Factory,用于Spring管理Job bean
        factory.setJobFactory(myJobFactory);
        return factory;
    }

    @Bean(name = "scheduler")
    public Scheduler scheduler() {
        return schedulerFactoryBean().getScheduler();
    }
}

MyJobFactory

@Component
public class MyJobFactory extends AdaptableJobFactory {

    //这个对象Spring会帮我们自动注入进来,也属于Spring技术范畴.
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类的方法
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入,这属于Spring的技术,不清楚的可以查看Spring的API.
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

依赖:

		<dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>

Controller层调用

@CrossOrigin
@RestController
@RequestMapping("/job")
@Api(tags = "4", description = "定时器管理")
public class JobController {
    @Autowired
    private RegularService regularService;
    @Autowired
    private JobService jobService;
    @Autowired
    private ProjectService projectService;
    @Autowired
    private JobTask jobTask;
    @PostMapping("/create")
    @ApiOperation(value = "创建定时任务", notes = "创建定时任务")
    @Transactional
    public Result create(String projectName, String startTime, String finishTime, String frequency, String srcVersion) {
        //实体类
        MyJob myJob = new MyJob ();
        String jobId = UUID.randomUUID().toString();
        myJob .setJobId(jobId);
        myJob .setProjectId(projectId);
        myJob .setStartTime(DateUtil.parse(startTime, DateUtil.YCHAR_06));
        myJob .setFinishTime(DateUtil.parse(finishTime, DateUtil.YCHAR_06));
        myJob .setFrequency(frequency);
        myJob .setSrcVersion(srcVersion);
        jobService.save(myJob );

        //执行定时任务
        jobTask.addJob(myJob);
        return Result.success();
    }
}

JobTask

@Component
@Slf4j
public class JobTask {

    @Autowired
    private JobService jobService;
    @Autowired
    private Scheduler scheduler;
    public void updateJob(Job myJob) {
        removeJob(myJob);
        addJob(myJob);
    }

    public void removeJob(MyJob myJob) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey("myTrigger" + myJob.getJobId(), "group1");

            scheduler.pauseTrigger(triggerKey);
            scheduler.unscheduleJob(triggerKey);
        } catch (SchedulerException e) {
            log.error("error", e);
        }
    }

    public void addJob(MyJob myJob) {

        try {
            JobDetail jobDetail = newJob(HelloJob.class)
                    .withIdentity("myJob" + myJob.getJobId())
                    .usingJobData("myJob", JSONObject.toJSONString(myJob))
                    .build();
            Date startTime = myJob.getStartTime();

            Date endTime = myJob.getFinishTime();
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("myTrigger" + myJob.getJobId(), "group1")
                    .startAt(startTime)
                    .endAt(endTime)
                    .withSchedule(CronScheduleBuilder.cronSchedule(myJob.getFrequency()))
                    .build();


            //将trigger和jobDetail加入这个调度
            scheduler.scheduleJob(jobDetail, trigger);

            //启动scheduler
            scheduler.start();
        } catch (Exception e) {
            log.error("error", e);
        }
    }
}

HelloJob

public class HelloJob implements Job {

    @Autowired
    private ProjectService projectService;
    @Autowired
    private RegularService regularService;
    @Autowired
    private CompareService compareService;
    @Autowired
    private InterfaceService interfaceService;
    
    @Override
    public void execute(JobExecutionContext context)  {
        //获取trigger
        Trigger trigger = context.getTrigger();
        //获取数据
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String jobJsonString = jobDataMap.getString("myJob");
        //fastJson  解析成具体实体类对象
        cn.XXX.ict.entity.Job job = JSONObject.parseObject(jobJsonString, cn.XXX.ict.entity.Job.class);
        //通过trigger获取job标识
        JobKey jobKey = trigger.getJobKey();

        //主要任务
        ......
       }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值