• spring-quartz整合


    摘要

    spring ,springboot整合quartz-2.3.2,实现spring管理jobBean
    本文不涉及 JDBC存储的方式,springboot yml配置也没有 可自行百度 谷歌

    本项目源码gitee地址 quartz-demo

    需求

    比如发送邮件消息 在夜晚空闲时大批量更新统计数据,定时更新数据

    1.0 spring scheduling

    在看quartz之前想要先说一下 spring自带的定时任务框架 spring-scheduling org.springframework.scheduling.annotation.Scheduled
    相比于quartz,spring scheduling更加的轻量级 使用配置非常的简单(基于注解开发) 是实现简单需求时的最佳选择

    1.1 开启scheduling配置

          <task:annotation-driven />
    

    或者配置类添加注解 @EnableScheduling 使用@Scheduled
    官方注释如下

      Processing of {@code @Scheduled} annotations is performed by
      registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be
      done manually or, more conveniently, through the {@code <task:annotation-driven/>}
      element or @{@link EnableScheduling} annotation.
    

    1.2 @Scheduled 内容

    其中 cron fixedDelay(fixedDelayString) fixedRate(fixedRateString) 这三个属性有且只能配置一个 配置错误会有类似提示

    2.0 下面说说 quartz的配置使用

           <!--  springboot项目引入  -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-quartz</artifactId>
            </dependency>
          <!-- spring项目引入  -->
          <dependency>
                <groupId>org.quartz-scheduler</groupId>
                <artifactId>quartz</artifactId>
                <version>2.3.2</version>
            </dependency>
            <dependency>
                <groupId>org.quartz-scheduler</groupId>
                <artifactId>quartz-jobs</artifactId>
                <version>2.3.2</version>
            </dependency>
    

    2.1 quartz api

    job 定时任务执行的业务代码层 可以通过实现job接口 或者 继承 QuartzJobBean 实现
    JobDetail 用来描述任务的分组,名称 可以通过jobbuilder(推荐) 或者 factoryBean实现,需要将job.class 传入JobDetail中
    trigger 执行任务的触发条件 子类SimpleTrigger,CronTrigger.可由TriggerBuilder构建可以设置 触发器的的名称 和分组 dataMap,trigger是载体SimpleScheduleBuilder,CronScheduleBuilder 这俩builder才是设置执行周期的类
    Scheduler 负责调度 job 含有 Trigger 和job信息 spring框架中 由 SchedulerFactoryBean创建
    JobListener job trigger Scheduler均有对应的Listener 在任务初始化,执行异常 ,执行结束 可以插入具体的动作 Listener 在 scheduler添加job时可以绑定 scheduler.getListenerManager().addJobListener(new OneJobListener());

    2.2 测试先行

    先写一个简单的测试类

    
    /**
     * demo class
     */
    public class HelloJob implements Job {
    
    	@Override
    	public void execute(JobExecutionContext context) throws JobExecutionException {
    
    		JobDataMap jobDataMap = context.getTrigger().getJobDataMap();
    		Object t1 = jobDataMap.get("t1");
    		Object t2 = jobDataMap.get("t2");
    		Object j1 = jobDataMap.get("j1");
    		Object j2 = jobDataMap.get("j2");
    
    		Object sv = null ;
    
    		try {
    
    			sv = context.getScheduler().getContext().get("skey");
    
    		}catch (Exception e){
    			e.printStackTrace();
    		}
    		String limiter = ":" ;
    
    		System.out.println(t1+limiter+j1);
    		System.out.println(t2+limiter+j2);
    		System.out.println(sv);
    
    		System.out.println("hello"+ LocalDateTime.now());
    
    
    	}
    }
    测试
    
    public class QuartzTestSchedule {
    
    	@Test
    	@SneakyThrows
    	public void  test01(){
    
    		Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    
    		scheduler.start();
    
    		scheduler.shutdown();
    
    	}
    	@Test
    	@SneakyThrows
    	public void test02(){
    
    		Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    
    		SchedulerContext context = scheduler.getContext();
    		context.put("skey","this is svalue");
    
    		SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
    				.withIdentity("trigger01", "group01")
    				.usingJobData("t1", "t1_value")
    				.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
    				.build();
    
    		JobDetail jobDetail1 = JobBuilder.newJob(HelloJob.class)
    				.usingJobData("j1", "j1_value")
    				.withIdentity("myjob", "jobgroup01")
    				.build();
    
    		scheduler.scheduleJob(jobDetail1, simpleTrigger);
    		scheduler.start();
    		//防止主线程结束 不执行定时任务
    		Thread.sleep(10_000);
    	}
    }
    

    控制台输出

    t1_value:null
    null:null
    this is svalue
    hello2020-12-27T16:57:01.508
    16:57:04.484 ['定时任务'_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobgroup01.myjob', class=site.culater.quartz.HelloJob
    16:57:04.484 ['定时任务'_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
    16:57:04.484 ['定时任务'_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobgroup01.myjob
    t1_value:null
    null:null
    this is svalue
    hello2020-12-27T16:57:04.484
    16:57:07.488 ['定时任务'_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobgroup01.myjob', class=site.culater.quartz.HelloJob
    16:57:07.488 ['定时任务'_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobgroup01.myjob
    t1_value:null
    null:null
    this is svalue
    

    3.0 创建demo项目

    首先我们要先创建 quartz demo项目 直接创建springboot项目 引入依赖 过程略......
    引入spring-boot-starter-quartz依赖后 因为springboot的自动配置 可以直接用quartz
    quartz 默认使用 内存存储方式 JDBC存储的方式本文不涉及,如果是分布式部署 必须使用jdbc存储的方式 .
    选择因项目需求定各有优劣

    使用 quartz.properties文件

    quartz-jar内置一份文件 位置:quartz-2.3.2.jar!orgquartzquartz.properties

    # 实例名称 标识 无意义可随意设置
    org.quartz.scheduler.instanceName: '定时任务'
    org.quartz.scheduler.instanceId: 'new-quartz'
    # 远程管理
    org.quartz.scheduler.rmi.export: false
    org.quartz.scheduler.rmi.proxy: false
    # 是否启用事务 企业级功能
    org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
    #线程池实现类
    org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
    # 线程池数
    org.quartz.threadPool.threadCount: 5
    # 执行优先级
    org.quartz.threadPool.threadPriority: 5
    #设置程序启动不执行
    org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
    # 未执行确认时间
    org.quartz.jobStore.misfireThreshold: 60000
    # 默认内存 存储任务数据
    org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
    

    quartz.properties配置文件并不是添加到resources目录下自动加载的 需要手动配置
    指定 SchedulerFactoryBean 使用自己创建的

          /**
    	 * 直接注入 Scheduler 是无效的
    	 * 对应的xml'配置
    	 * <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    	 *     <property name="configLocation" value="classpath:quartz.properties" />
    	 *     // ...
    	 * </bean>
    	 * @return
    	 */
    	@Primary
    	@SneakyThrows
    	@Bean
    	public SchedulerFactoryBean schedule(CulaterSpringBeanJobFactory culaterSpringBeanJobFactory){
    
    		SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
    
    		ClassPathResource configLocation = new ClassPathResource("quartz.properties");
    
    		schedulerFactoryBean.setConfigLocation(configLocation);
    		//配置spring管理创建 job 不必每次执行实例 创建job实例
    		schedulerFactoryBean.setJobFactory(culaterSpringBeanJobFactory);
    //		schedulerFactoryBean.afterPropertiesSet();
    		return schedulerFactoryBean ;
    	}
    
    

    项目启动日志 可以看到设置的定时任务实例名称

    2020-12-27 15:20:48.816  INFO 22532 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler ''定时任务'' initialized from an externally provided properties instance.
    2020-12-27 15:20:48.816  INFO 22532 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
    2020-12-27 15:20:48.816  INFO 22532 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: site.culater.quartz.config.CulaterSpringBeanJobFactory@1aa61f3
    2020-12-27 15:20:48.969  INFO 22532 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'
    2020-12-27 15:20:49.001  INFO 22532 --- [           main] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now
    2020-12-27 15:20:49.001  INFO 22532 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler '定时任务'_$_'new-quartz' started.
    2020-12-27 15:20:49.016  INFO 22532 --- [           main] site.culater.quartz.QuartzApplication    : Started QuartzApplication in 2.02 seconds (JVM running for 4.17)
    

    3.1 schedulerFactoryBean

    schedulerFactoryBean 用来创建 Scheduler ,创建完成后再对 schedulerFactoryBean 是无效的,
    但是我们从 spring容器中获得schedulerFactoryBean, get Scheduler是唯一的, 通过scheduler可以动态的添加 修改 删除 job的执行

    创建 triggerBean配置类

    /**
     * 创建Trigger jobdetail使用
     */
    @Configuration
    public class TtriggerBean {
    
    	@Bean("CulaterJob01")
    	public CulaterJob getCulaterJob01(){
    
    		JobDetail jobDetail = JobBuilder.newJob(OneJob.class).build();
    		SimpleTrigger trigger = TriggerBuilder.newTrigger()
    				.withSchedule(SimpleScheduleBuilder.simpleSchedule().withRepeatCount(10).withIntervalInSeconds(3))
    				//.forJob(jobDetail)
    				.startNow().build();
    
    		CulaterJob culaterJob = CulaterJob.builder().trigger(trigger).jobDetail(jobDetail).build();
    		return culaterJob;
    	}
    
    	@Bean("CulaterJob02")
    	public CulaterJob getCulaterJob02(){
    
    		JobDetail jobDetail = JobBuilder.newJob(OneJob02.class).build();
    		SimpleTrigger trigger = TriggerBuilder.newTrigger()
    				.withSchedule(SimpleScheduleBuilder.simpleSchedule().withRepeatCount(10).withIntervalInSeconds(3))
    				//.forJob(jobDetail)
    				.startNow().build();
    
    		CulaterJob culaterJob = CulaterJob.builder().trigger(trigger).jobDetail(jobDetail).build();
    		return culaterJob;
    	}
    }
    

    TriggerStart实现了ApplicationRunner接口 springboot在项目启动后 会自动执行run方法,
    通过构造方法(lombok注解)注入 的 schedulerFactoryBean获取Scheduler
    culaterJobList 获得所有的 CulaterJob在spring容器中的所有对象实例

    trigger JobBuilder.*.forJob(jobDetail) 这种绑定任务的方法是无效的,必须使用 Scheduler同时添加 jobdetail和trigger
    如果没有添加job 则会提示job不能为null ,job可以继承QuartzJobBean

    注意:不能使用接口匿名内部类 或者内部类的方式创建Job 否则提示 newJob 方法执行失败

    所以创建了 CulaterJob 用来传递上述jobdetail,trigger对象

    /**
     * springboot启动后添加定时任务
     */
    @Component
    @RequiredArgsConstructor
    public class TriggerStart implements ApplicationRunner {
    
    	private final SchedulerFactoryBean schedulerFactoryBean;
    
    	private final List<CulaterJob> culaterJobList ;
    
    	@Override
    	public void run(ApplicationArguments args) throws Exception {
    
    		Scheduler scheduler = schedulerFactoryBean.getScheduler();
    		for (CulaterJob culaterJob : culaterJobList) {
    			scheduler.scheduleJob(culaterJob.getJobDetail(),culaterJob.getTrigger());
    		}
    
    	}
    }
    

    这样一个定时任务就配置完成了 可以运行试试哦,下面我们看看 quartz的任务执行

    3.2 从 SchedulerFactoryBean 看quartz

    quartz默认每次定时任务运行时创建新的job实例执行后丢弃掉
    SchedulerFactoryBean从字面上看就知道是创建Scheduler的工厂方法,这是spring 官方提供的
    SchedulerFactoryBean在创建的时候可以设置读取 properties,可以配置jdbc 数据源
    其中一个方法 setJobFactory 如果不设置 则默认AdaptableJobFactory

          prepareScheduler(){
          .....
    Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName);
    			populateSchedulerContext(scheduler);
    
    			if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) {
    				// Use AdaptableJobFactory as default for a local Scheduler, unless when
    				// explicitly given a null value through the "jobFactory" bean property.
                                    // 此处设置 默认
    				this.jobFactory = new AdaptableJobFactory();
    			}
    			if (this.jobFactory != null) {
    				if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) {
    					((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext);
    				}
    				if (this.jobFactory instanceof SchedulerContextAware) {
    					((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext());
    				}
    				scheduler.setJobFactory(this.jobFactory);
    			}
    			return scheduler;
          
    }
    AdaptableJobFactory中创建实例的方法,打断点可以看到每次执行都会创建新的job实例
    ```java
    
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
    		Class<?> jobClass = bundle.getJobDetail().getJobClass();
    		return ReflectionUtils.accessibleConstructor(jobClass).newInstance();
    	}
    

    添加测试类打印job类地址

    public class OneJob02 extends QuartzJobBean {
    	@Override
    	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    
    		Job jobInstance = context.getJobInstance();
    		System.out.println("OneTrigger----00002->"+jobInstance);
    	}
    
    }
    

    控制台打印 可以看到每次打印的地址都不同 每次执行job的实例都是新创建的

    OneTrigger----00002->site.culater.quartz.job.OneJob02@fdf5a4
    OneTrigger----00002->site.culater.quartz.job.OneJob02@4b06ee
    OneTrigger----00002->site.culater.quartz.job.OneJob02@3c4abb
    OneTrigger----00002->site.culater.quartz.job.OneJob02@8ce917
    OneTrigger----00002->site.culater.quartz.job.OneJob02@5806db
    

    3.3 改进job实例创建 托管spring

    如果定时任务执行的很频繁 我们不希望频繁的创建销毁实例 可以 继承SpringBeanJobFactory 重写 createJobInstance方法
    schedulerFactoryBean.setJobFactory(culaterSpringBeanJobFactory);
    只要任务通过spring创建实例则不需要再创建 否则创建新的 实例 也可以都创建实例 将单例多例的控制交给spring

    @Component
    public class CulaterSpringBeanJobFactory extends SpringBeanJobFactory {
    
    	@Autowired
    	ApplicationContext applicationContext;
    
    	@Override
    	protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
    
    		AutowireCapableBeanFactory autowireCapableBeanFactory = applicationContext.getAutowireCapableBeanFactory();
    		Class<? extends Job> jobClass = bundle.getJobDetail().getJobClass();
    
    		Object jobInstance = null;
    		try {
    			//如果可以从spring容器中获得job实例则不需调用父方法创建
    			jobInstance = autowireCapableBeanFactory.getBean(jobClass);
    		}
    		catch (BeansException e) {
    			// 此处屏蔽异常 没有找到更好的根据class 获得bean的方法
    		}
    		if (jobInstance == null) {
    			jobInstance = super.createJobInstance(bundle);
    		}
    		return jobInstance;
    	}
    }
    
    

    3.5 控制并发调度

    quartz 默认是并发调度 可能会出现 上一个定时任务执行时间过长还没结束下一个任务就开始了
    如果没有这方面的需求 则可以在job子类添加@DisallowConcurrentExecution 关闭并发执行
    最后控制台的输出如下: 可以看到 OneJob一直是同一个实例 而且不进行并发调度,OneJob02每次都会创建一个新的实例

    OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
    OneTrigger----00002->site.culater.quartz.job.OneJob02@fdf5a4
    OneTrigger----00002->site.culater.quartz.job.OneJob02@4b06ee
    OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
    OneTrigger----00002->site.culater.quartz.job.OneJob02@3c4abb
    OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
    OneTrigger----00002->site.culater.quartz.job.OneJob02@8ce917
    OneTrigger----00002->site.culater.quartz.job.OneJob02@5806db
    

    4 使用idea开发能遇到的问题

    1. 控制台输出中文乱码 可以通过 File Encoding 设置 项目和系统编码为utf-8
    2. quartz.properties文件没有被更新至 编译后的目录 这时候可以 重新rebuild项目 然后增加删除文件就可以同步更新了(稍有延迟)
    本项目源码gitee地址 quartz-demo

    扑克牌的四种花色分别叫红桃、梅花、方块和黑桃。
    The suits are called hearts, clubs, diamonds and spades

  • 相关阅读:
    基本指令
    javascript event(事件对象)详解
    Sass进阶之路,之二(进阶篇)
    Sass进阶之路,之一(基础篇)
    原型链进阶
    数据类型检测
    JavaScript引用类型和值类型
    i.mx6 Android6.0.1分析input子系统:测试
    (三)JNI常用示例
    (二)JNI方法总结
  • 原文地址:https://www.cnblogs.com/cu-later/p/14198071.html
Copyright © 2020-2023  润新知