一 定时任务介绍

自律是很多人都想拥有的一种能力,或者说素质,但是理想往往很美好,现实却是无比残酷的。在现实生活中,我们很难做到自律,或者说做到持续自律。例如,我们经常会做各种学习计划、储蓄计划或减肥计划等,但无一例外地被各种“意外”打破。这往往使得我们非常沮丧,甚至开始怀疑人生。

但是有一个“家伙”在自律方面做得格外出色。它只要制订了计划就会严格地执行,而且无论一个任务重复多少遍都不厌其烦,简直自律到“令人发指”,它就是定时任务。

1.1 什么时候需要定时任务

哪些业务场景适合使用定时任务呢?简单概括一下就是:at sometime to do something.凡是在某一时刻需要做某件事情时,都可以考虑使用定时任务(非实时性需求)。
定时任务常见业务场景

  • 银行月底汇总账单
  • 电信公司月底结算话费
  • 订单在30分钟内未支付会自动取消(延时任务)
  • 商品详情、文章的缓存定时更新
  • 定时同步跨库的数据库表数据

1.2 java中的定时任务

1.2.1 单机环境

  • Timer:来自JDK,从JDK 1.3开始引入。JDK自带,不需要引入外部依赖,简单易用,但是功能相对单一。
  • ScheduledExecutorService:同样来自JDK,比Timer晚一些,从JDK 1.5开始引入,它的引入弥补了Timer的一些缺陷。
  • Spring Task:来自Spring,Spring环境中单机定时任务的不二之选。

1.2.2 分布式环境

  • Quartz:一个完全由 Java 编写的开源作业调度框架,分布式定时任务的基石,功能丰富且强大,既能与简单的单体应用结合,又能支撑起复杂的分布式系统。
  • ElasticJob:来自当当网,最开始是基于Quartz开发的,后来改用ZooKeeper来实现分布式协调。它具有完整的定时任务处理流程,很多国内公司都在使用(目前登记在册的有80多家),并且支持云开发。
  • XXL-JOB:来自大众点评,同样是基于Quartz开发的,后来改用自研的调度组件。它是一个轻量级的分布式任务调度平台,简单易用,很多国内公司都在使用(目前登记在册的有400多家)。
  • PowerJob:号称“全新一代分布式调度与计算框架”,采用无锁化设计,支持多种报警通知方式(如WebHook、邮件、钉钉及自定义)。它比较重量级,适合做公司公共的任务调度中间件。

二 Quartz介绍

2.1 核心概念

  • Job:是一个接口,表示一个工作,要具体执行的内容,任务的核心逻辑。该接口只有一个excute方法
  • JobDetail:对Job进一步封装,一个具体的可执行的调度程序。包含了任务的调度方案和策略。JobDetail既然是通用任务,用于接受任务,所以我们需要定义一个自己的任务类(例如叫做QuartzDetailJob),这个任务类需要实现 Job接口,这个任务类QuartzDetailJob,要执行具体的任务,具体的任务,一般都是我们写的自己的一些方法
  • Trigger:触发器,调度参数的配置,配置什么时候去调定时任务。主要用来指定Job的触发规则,分为SimpleTrigger和CronTrigger(这里使用的是常用的CronTrigger)
  • Scheduler:调度容器(调度中心,任务交给它就行了),一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。。用来维护Job的生命周期(创建、删除、暂停、调度等)

2.1 SpringBoot单机版-整合Quartz代码实战

数据库表结构官网已经提供,我们可以直接访问Quartz对应的官网下载,找到对应的版本,然后将其下载!目前最新的稳定版是2.3.0,我们就下载这个版本

在这里插入图片描述
下载完成之后将其解压,在文件中搜索sql,在里面选择适合当前环境的数据库脚本文件,然后将其初始化到数据库中即可!我这里使用的是mysql,所以使用tables_mysql_innodb.sql这个脚本
在这里插入图片描述
把里边的sql语句,在mysql库里执行即可。共涉及到11张表,每张表的含义如下
在这里插入图片描述
其中,QRTZ_LOCKS 就是 Quartz 集群实现同步机制的行锁表

引入依赖

<!--定时任务-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--druid 数据连接池-->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>

新建quartz.properties 配置文件(数据库信息换为自己的数据库即可)

#调度配置
#调度器实例名称
org.quartz.scheduler.instanceName=SsmScheduler
#调度器实例编号自动生成
org.quartz.scheduler.instanceId=AUTO
#是否在Quartz执行一个job前使用UserTransaction
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false#线程池配置
#线程池的实现类
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#线程池中的线程数量
org.quartz.threadPool.threadCount=10
#线程优先级
org.quartz.threadPool.threadPriority=5
#配置是否启动自动加载数据库内的定时任务,默认true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
#是否设置为守护线程,设置后任务将不会执行
#org.quartz.threadPool.makeThreadsDaemons=true#持久化方式配置
#JobDataMaps是否都为String类型
org.quartz.jobStore.useProperties=true
#数据表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix=QRTZ_
#最大能忍受的触发超时时间
org.quartz.jobStore.misfireThreshold=60000
#是否以集群方式运行
org.quartz.jobStore.isClustered=true
#调度实例失效的检查时间间隔,单位毫秒
org.quartz.jobStore.clusterCheckinInterval=2000
#数据保存方式为数据库持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
#数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#数据库别名 随便取
org.quartz.jobStore.dataSource=qzDS#数据库连接池,将其设置为druid
org.quartz.dataSource.qzDS.connectionProvider.class=com.ts.hjbz.quartz.DruidConnectionProvider
#数据库引擎
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
#数据库连接
org.quartz.dataSource.qzDS.URL=jdbc:mysql://192.168.119.128:3306/hjbz?serverTimezone=GMT%2B8&characterEncoding=utf-8
#数据库用户
org.quartz.dataSource.qzDS.user=root
#数据库密码
org.quartz.dataSource.qzDS.password=123456
#允许最大连接
org.quartz.dataSource.qzDS.maxConnection=5
#验证查询sql,可以不设置
org.quartz.dataSource.qzDS.validationQuery=select 0 from dual

注册 Quartz 任务工厂

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;/*** @Author:sgw* @Date:2023/9/1* @Description: 注册 Quartz 任务工厂*/
@Component
public class QuartzJobFactory extends AdaptableJobFactory {@Autowiredprivate AutowireCapableBeanFactory capableBeanFactory;@Overrideprotected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {//调用父类的方法Object jobInstance = super.createJobInstance(bundle);//进行注入capableBeanFactory.autowireBean(jobInstance);return jobInstance;}
}

注册调度工厂

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;import java.io.IOException;/*** @Author:sgw* @Date:2023/9/1* @Description:注册调度工厂*/
@Configuration
public class QuartzConfig {@Autowiredprivate QuartzJobFactory jobFactory;@Beanpublic SchedulerFactoryBean schedulerFactoryBean() throws IOException {//获取配置属性PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));//在quartz.properties中的属性被读取并注入后再初始化对象propertiesFactoryBean.afterPropertiesSet();//创建SchedulerFactoryBeanSchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setQuartzProperties(propertiesFactoryBean.getObject());factory.setJobFactory(jobFactory);//支持在JOB实例中注入其他的业务对象factory.setApplicationContextSchedulerContextKey("applicationContextKey");factory.setWaitForJobsToCompleteOnShutdown(true);//这样当spring关闭时,会等待所有已经启动的quartz job结束后spring才能完全shutdown。factory.setOverwriteExistingJobs(false);//是否覆盖己存在的Jobfactory.setStartupDelay(10);//QuartzScheduler 延时启动,应用启动完后 QuartzScheduler 再启动return factory;}/*** 通过SchedulerFactoryBean获取Scheduler的实例* @return* @throws IOException* @throws SchedulerException*/@Bean(name = "scheduler")public Scheduler scheduler() throws IOException, SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();return scheduler;}
}

重新设置 Quartz 数据连接池
默认 Quartz 的数据连接池是 c3p0,由于性能不太稳定,不推荐使用,因此我们将其改成driud数据连接池,配置如下:

import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.SchedulerException;
import org.quartz.utils.ConnectionProvider;import java.sql.Connection;
import java.sql.SQLException;/*** @Author:sgw* @Date:2023/9/1* @Description: 重新设置 Quartz 数据连接池。默认 Quartz 的数据连接池是 c3p0,由于性能不太稳定,不推荐使用,因此我们将其改成driud数据连接池*/
public class DruidConnectionProvider implements ConnectionProvider {/*** 常量配置,与quartz.properties文件的key保持一致(去掉前缀),同时提供set方法,Quartz框架自动注入值。* @return* @throws SQLException*///JDBC驱动public String driver;//JDBC连接串public String URL;//数据库用户名public String user;//数据库用户密码public String password;//数据库最大连接数public int maxConnection;//数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。public String validationQuery;private boolean validateOnCheckout;private int idleConnectionValidationSeconds;public String maxCachedStatementsPerConnection;private String discardIdleConnectionsSeconds;public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;//Druid连接池private DruidDataSource datasource;@Overridepublic Connection getConnection() throws SQLException {return datasource.getConnection();}@Overridepublic void shutdown() throws SQLException {datasource.close();}@Overridepublic void initialize() throws SQLException {if (this.URL == null) {throw new SQLException("DBPool could not be created: DB URL cannot be null");}if (this.driver == null) {throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");}if (this.maxConnection < 0) {throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");}datasource = new DruidDataSource();try{datasource.setDriverClassName(this.driver);} catch (Exception e) {try {throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);} catch (SchedulerException e1) {}}datasource.setUrl(this.URL);datasource.setUsername(this.user);datasource.setPassword(this.password);datasource.setMaxActive(this.maxConnection);datasource.setMinIdle(1);datasource.setMaxWait(0);datasource.setMaxPoolPreparedStatementPerConnectionSize(DEFAULT_DB_MAX_CONNECTIONS);if (this.validationQuery != null) {datasource.setValidationQuery(this.validationQuery);if(!this.validateOnCheckout)datasource.setTestOnReturn(true);elsedatasource.setTestOnBorrow(true);datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);}}public String getDriver() {return driver;}public void setDriver(String driver) {this.driver = driver;}public String getURL() {return URL;}public void setURL(String URL) {this.URL = URL;}public String getUser() {return user;}public void setUser(String user) {this.user = user;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public int getMaxConnection() {return maxConnection;}public void setMaxConnection(int maxConnection) {this.maxConnection = maxConnection;}public String getValidationQuery() {return validationQuery;}public void setValidationQuery(String validationQuery) {this.validationQuery = validationQuery;}public boolean isValidateOnCheckout() {return validateOnCheckout;}public void setValidateOnCheckout(boolean validateOnCheckout) {this.validateOnCheckout = validateOnCheckout;}public int getIdleConnectionValidationSeconds() {return idleConnectionValidationSeconds;}public void setIdleConnectionValidationSeconds(int idleConnectionValidationSeconds) {this.idleConnectionValidationSeconds = idleConnectionValidationSeconds;}public DruidDataSource getDatasource() {return datasource;}public void setDatasource(DruidDataSource datasource) {this.datasource = datasource;}public String getDiscardIdleConnectionsSeconds() {return discardIdleConnectionsSeconds;}public void setDiscardIdleConnectionsSeconds(String discardIdleConnectionsSeconds) {this.discardIdleConnectionsSeconds = discardIdleConnectionsSeconds;}
}

创建完成之后,还需要在quartz.properties配置文件中设置一下即可!

#数据库连接池,将其设置为druid
org.quartz.dataSource.qzDS.connectionProvider.class=com.ts.hjbz.quartz.DruidConnectionProvider

编写 Job 具体任务类(不同的任务,需要定义不同的job类)

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.text.SimpleDateFormat;
import java.util.Date;/*** @Author:sgw* @Date:2023/9/1* @Description: 具体要执行的的job*/
public class TfCommandJob implements Job {private static final Logger log = LoggerFactory.getLogger(TfCommandJob.class);@Overridepublic void execute(JobExecutionContext context) {try {System.out.println("开始执行:"+context.getScheduler().getSchedulerInstanceId() + "--" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));} catch (SchedulerException e) {log.error("任务执行失败",e);}}
}

编写 Quartz 服务层接口

import java.util.Map;public interface QuartzJobService {/*** 添加任务可以传参数* @param clazzName* @param jobName* @param groupName* @param cronExp* @param param*/void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param);/*** 暂停任务* @param jobName* @param groupName*/void pauseJob(String jobName, String groupName);/*** 恢复任务* @param jobName* @param groupName*/void resumeJob(String jobName, String groupName);/*** 立即运行一次定时任务* @param jobName* @param groupName*/void runOnce(String jobName, String groupName);/*** 更新任务* @param jobName* @param groupName* @param cronExp* @param param*/void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param);/*** 删除任务* @param jobName* @param groupName*/void deleteJob(String jobName, String groupName);/*** 启动所有任务*/void startAllJobs();/*** 暂停所有任务*/void pauseAllJobs();/*** 恢复所有任务*/void resumeAllJobs();/*** 关闭所有任务*/void shutdownAllJobs();
}

对应的实现类QuartzJobServiceImpl如下:

import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Map;/*** @Author:sgw* @Date:2023/9/1* @Description:*/
@Service
public class QuartzJobServiceImpl implements QuartzJobService {private static final Logger log = LoggerFactory.getLogger(QuartzJobServiceImpl.class);@Autowiredprivate Scheduler scheduler;@Overridepublic void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param) {try {// 启动调度器,默认初始化的时候已经启动
//            scheduler.start();//构建job信息Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(clazzName);JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, groupName).build();//表达式调度构建器(即任务执行的时间)CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);//按新的cronExpression表达式构建一个新的triggerCronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, groupName).withSchedule(scheduleBuilder).build();//获得JobDataMap,写入数据if (param != null) {trigger.getJobDataMap().putAll(param);}scheduler.scheduleJob(jobDetail, trigger);} catch (Exception e) {log.error("创建任务失败", e);}}@Overridepublic void pauseJob(String jobName, String groupName) {try {scheduler.pauseJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("暂停任务失败", e);}}@Overridepublic void resumeJob(String jobName, String groupName) {try {scheduler.resumeJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("恢复任务失败", e);}}@Overridepublic void runOnce(String jobName, String groupName) {try {scheduler.triggerJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("立即运行一次定时任务失败", e);}}@Overridepublic void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param) {try {TriggerKey triggerKey = TriggerKey.triggerKey(jobName, groupName);CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);if (cronExp != null) {// 表达式调度构建器CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);// 按新的cronExpression表达式重新构建triggertrigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();}//修改mapif (param != null) {trigger.getJobDataMap().putAll(param);}// 按新的trigger重新设置job执行scheduler.rescheduleJob(triggerKey, trigger);} catch (Exception e) {log.error("更新任务失败", e);}}@Overridepublic void deleteJob(String jobName, String groupName) {try {//暂停、移除、删除scheduler.pauseTrigger(TriggerKey.triggerKey(jobName, groupName));scheduler.unscheduleJob(TriggerKey.triggerKey(jobName, groupName));scheduler.deleteJob(JobKey.jobKey(jobName, groupName));} catch (Exception e) {log.error("删除任务失败", e);}}@Overridepublic void startAllJobs() {try {scheduler.start();} catch (Exception e) {log.error("开启所有的任务失败", e);}}@Overridepublic void pauseAllJobs() {try {scheduler.pauseAll();} catch (Exception e) {log.error("暂停所有任务失败", e);}}@Overridepublic void resumeAllJobs() {try {scheduler.resumeAll();} catch (Exception e) {log.error("恢复所有任务失败", e);}}@Overridepublic void shutdownAllJobs() {try {if (!scheduler.isShutdown()) {// 需谨慎操作关闭scheduler容器// scheduler生命周期结束,无法再 start() 启动schedulerscheduler.shutdown(true);}} catch (Exception e) {log.error("关闭所有的任务失败", e);}}
}

创建一个请求参数实体类

import lombok.Data;import java.io.Serializable;
import java.util.Map;/*** @Author:sgw* @Date:2023/9/1* @Description:*/
@Data
public class QuartzConfigDTO implements Serializable {private static final long serialVersionUID = 1L;/*** 任务名称*/private String jobName;/*** 任务所属组*/private String groupName;/*** 任务执行类*/private String jobClass;/*** 任务调度时间表达式*/private String cronExpression;/*** 附加参数*/private Map<String, Object> param;
}

编写 contoller 服务

import com.ts.hjbz.quartz.QuartzConfigDTO;
import com.ts.hjbz.quartz.QuartzJobService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
/*** @Author:sgw* @Date:2023/9/1* @Description: 定时任务入口类*/
@RestController
@RequestMapping("/job")
public class QuartzController {private static final Logger log = LoggerFactory.getLogger(QuartzController.class);@Autowiredprivate QuartzJobService quartzJobService;/*** 添加新任务* @param configDTO* @return*/@RequestMapping("/addJob")public Object addJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.addJob(configDTO.getJobClass(), configDTO.getJobName(), configDTO.getGroupName(), configDTO.getCronExpression(), configDTO.getParam());return HttpStatus.OK;}/*** 暂停任务* @param configDTO* @return*/@RequestMapping("/pauseJob")public Object pauseJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.pauseJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 恢复任务* @param configDTO* @return*/@RequestMapping("/resumeJob")public Object resumeJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.resumeJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 立即运行一次定时任务* @param configDTO* @return*/@RequestMapping("/runOnce")public Object runOnce(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.runOnce(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 更新任务* @param configDTO* @return*/@RequestMapping("/updateJob")public Object updateJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.updateJob(configDTO.getJobName(), configDTO.getGroupName(), configDTO.getCronExpression(), configDTO.getParam());return HttpStatus.OK;}/*** 删除任务* @param configDTO* @return*/@RequestMapping("/deleteJob")public Object deleteJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.deleteJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 启动所有任务* @return*/@RequestMapping("/startAllJobs")public Object startAllJobs() {quartzJobService.startAllJobs();return HttpStatus.OK;}/*** 暂停所有任务* @return*/@RequestMapping("/pauseAllJobs")public Object pauseAllJobs() {quartzJobService.pauseAllJobs();return HttpStatus.OK;}/*** 恢复所有任务* @return*/@RequestMapping("/resumeAllJobs")public Object resumeAllJobs() {quartzJobService.resumeAllJobs();return HttpStatus.OK;}/*** 关闭所有任务* @return*/@RequestMapping("/shutdownAllJobs")public Object shutdownAllJobs() {quartzJobService.shutdownAllJobs();return HttpStatus.OK;}
}

使用postman进行测试,新增一个定时任务,每隔五秒执行一次
在这里插入图片描述
具体参数如下

{"jobName":"测试任务1","groupName":"组1","cronExpression":"0/5 * * * * ? ","jobClass":"com.ts.hjbz.quartz.TfCommandJob","param":{"hello":"hello啊"}
}

其中cronExpression,直接上网查询在线cron表达式转换即可,如https://www.bejson.com/othertools/cron/
在这里插入图片描述
上图是配置每5秒执行一次,生成的cron表达式就是我们postman里需要的cronExpression参数。执行postman的调用后,可以看到控制台每隔5秒打印一次,如下

在这里插入图片描述

并且服务重启后,这个定时任务依然存在,依然会每隔5秒执行一次。

注册监听器(选用)

如果你想在 SpringBoot 里面集成 Quartz 的监听器,操作也很简单

创建任务调度监听器

@Component
public class SimpleSchedulerListener extends SchedulerListenerSupport {@Overridepublic void jobScheduled(Trigger trigger) {System.out.println("任务被部署时被执行");}@Overridepublic void jobUnscheduled(TriggerKey triggerKey) {System.out.println("任务被卸载时被执行");}@Overridepublic void triggerFinalized(Trigger trigger) {System.out.println("任务完成了它的使命,光荣退休时被执行");}@Overridepublic void triggerPaused(TriggerKey triggerKey) {System.out.println(triggerKey + "(一个触发器)被暂停时被执行");}@Overridepublic void triggersPaused(String triggerGroup) {System.out.println(triggerGroup + "所在组的全部触发器被停止时被执行");}@Overridepublic void triggerResumed(TriggerKey triggerKey) {System.out.println(triggerKey + "(一个触发器)被恢复时被执行");}@Overridepublic void triggersResumed(String triggerGroup) {System.out.println(triggerGroup + "所在组的全部触发器被回复时被执行");}@Overridepublic void jobAdded(JobDetail jobDetail) {System.out.println("一个JobDetail被动态添加进来");}@Overridepublic void jobDeleted(JobKey jobKey) {System.out.println(jobKey + "被删除时被执行");}@Overridepublic void jobPaused(JobKey jobKey) {System.out.println(jobKey + "被暂停时被执行");}@Overridepublic void jobsPaused(String jobGroup) {System.out.println(jobGroup + "(一组任务)被暂停时被执行");}@Overridepublic void jobResumed(JobKey jobKey) {System.out.println(jobKey + "被恢复时被执行");}@Overridepublic void jobsResumed(String jobGroup) {System.out.println(jobGroup + "(一组任务)被恢复时被执行");}@Overridepublic void schedulerError(String msg, SchedulerException cause) {System.out.println("出现异常" + msg + "时被执行");cause.printStackTrace();}@Overridepublic void schedulerInStandbyMode() {System.out.println("scheduler被设为standBy等候模式时被执行");}@Overridepublic void schedulerStarted() {System.out.println("scheduler启动时被执行");}@Overridepublic void schedulerStarting() {System.out.println("scheduler正在启动时被执行");}@Overridepublic void schedulerShutdown() {System.out.println("scheduler关闭时被执行");}@Overridepublic void schedulerShuttingdown() {System.out.println("scheduler正在关闭时被执行");}@Overridepublic void schedulingDataCleared() {System.out.println("scheduler中所有数据包括jobs, triggers和calendars都被清空时被执行");}
}

创建任务触发监听器

@Component
public class SimpleTriggerListener extends TriggerListenerSupport {/*** Trigger监听器的名称* @return*/@Overridepublic String getName() {return "mySimpleTriggerListener";}/*** Trigger被激发 它关联的job即将被运行* @param trigger* @param context*/@Overridepublic void triggerFired(Trigger trigger, JobExecutionContext context) {System.out.println("myTriggerListener.triggerFired()");}/*** Trigger被激发 它关联的job即将被运行, TriggerListener 给了一个选择去否决 Job 的执行,如果返回TRUE 那么任务job会被终止* @param trigger* @param context* @return*/@Overridepublic boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {System.out.println("myTriggerListener.vetoJobExecution()");return false;}/*** 当Trigger错过被激发时执行,比如当前时间有很多触发器都需要执行,但是线程池中的有效线程都在工作,* 那么有的触发器就有可能超时,错过这一轮的触发。* @param trigger*/@Overridepublic void triggerMisfired(Trigger trigger) {System.out.println("myTriggerListener.triggerMisfired()");}/*** 任务完成时触发* @param trigger* @param context* @param triggerInstructionCode*/@Overridepublic void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {System.out.println("myTriggerListener.triggerComplete()");}
}

创建任务执行监听器

@Component
public class SimpleJobListener extends JobListenerSupport {/*** job监听器名称* @return*/@Overridepublic String getName() {return "mySimpleJobListener";}/*** 任务被调度前* @param context*/@Overridepublic void jobToBeExecuted(JobExecutionContext context) {System.out.println("simpleJobListener监听器,准备执行:"+context.getJobDetail().getKey());}/*** 任务调度被拒了* @param context*/@Overridepublic void jobExecutionVetoed(JobExecutionContext context) {System.out.println("simpleJobListener监听器,取消执行:"+context.getJobDetail().getKey());}/*** 任务被调度后* @param context* @param jobException*/@Overridepublic void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {System.out.println("simpleJobListener监听器,执行结束:"+context.getJobDetail().getKey());}
}

最后,在QuartzConfig中将监听器注册到Scheduler

@Autowired
private SimpleSchedulerListener simpleSchedulerListener;@Autowired
private SimpleJobListener simpleJobListener;@Autowired
private SimpleTriggerListener simpleTriggerListener;@Bean(name = "scheduler")
public Scheduler scheduler() throws IOException, SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();//全局添加监听器//添加SchedulerListener监听器scheduler.getListenerManager().addSchedulerListener(simpleSchedulerListener);// 添加JobListener, 支持带条件匹配监听器scheduler.getListenerManager().addJobListener(simpleJobListener, KeyMatcher.keyEquals(JobKey.jobKey("myJob", "myGroup")));// 添加triggerListener,设置全局监听scheduler.getListenerManager().addTriggerListener(simpleTriggerListener, EverythingMatcher.allTriggers());return scheduler;
}

采用项目数据源(选用)
在上面的 Quartz 数据源配置中,我们使用了自定义的数据源,目的是和项目中的数据源实现解耦,当然有的同学不想单独建库,想和项目中数据源保持一致,配置也很简单!
quartz.properties配置文件中,去掉org.quartz.jobStore.dataSource配置,即

#注释掉quartz的数据源配置
#org.quartz.jobStore.dataSource=qzDS

在QuartzConfig配置类中加入dataSource数据源,并将其注入到quartz中

@Autowired
private DataSource dataSource;@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {//...SchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setQuartzProperties(propertiesFactoryBean.getObject());//使用数据源,自定义数据源factory.setDataSource(dataSource);//...return factory;
}

2.2 SpringBoot集群版-整合Quartz代码实战

Quartz 提供了极为广用的特性,如任务持久化、集群部署和分布式调度任务等等,正因如此,基于 Quartz 任务调度功能在系统开发中应用极为广泛!

在集群环境下,Quartz 集群中的每个节点是一个独立的 Quartz 应用,没有负责集中管理的节点,而是通过数据库表来感知另一个应用,利用数据库锁的方式来实现集群环境下进行并发控制,每个任务当前运行的有效节点有且只有一个!

特别需要注意的是:分布式部署时需要保证各个节点的系统时间一致!

在实际的部署中,项目都是集群进行部署,因此为了和正式环境一致,我们再新建两个相同的项目来测试一下在集群环境下 quartz 是否可以实现分布式调度,保证任何一个定时任务只有一台机器在运行?理论上,我们只需要将刚刚新建好的项目,重新复制一份,然后修改一下端口号就可以实现本地测试!

因为curd服务只需要一个,因此我们在新的服务里,不需要再编写QuartzJobService等增、删、改服务,仅仅保持QuartzConfig、DruidConnectionProvider、QuartzJobFactory、TfCommandJob、quartz.properties类和配置都是相同的就可以了!

依次启动服务quartz-001、quartz-002、quartz-003,看看效果如何

第一个启动的服务quartz-001会优先加载数据库中已经配置好的定时任务,其他两个服务quartz-002、quartz-003都没有主动调度服务;

当我们主动关闭quartz-001时,quartz-002服务主动接收任务调度

当我们主动关闭quartz-002,同样quartz-003服务主动接收任务调度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91076.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91076.shtml
英文地址,请注明出处:http://en.pswp.cn/web/91076.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java中的异常判断以及文件中的常用方法及功能

目录 异常 作用 异常的处理方式 JVM&#xff08;虚拟机&#xff09;默认的处理方式 自己处理&#xff08;捕获异常&#xff09; 抛出异常&#xff08;也就是交给调用者处理&#xff09; 自定义异常 file File中常见成员方法 判断和获取 创建和删除 获取并遍历 异常…

【C++算法】74.优先级队列_最后一块石头的重量

文章目录题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;题目链接&#xff1a; 1046. 最后一块石头的重量 题目描述&#xff1a; 解法 每次取出最重的两块石头进行碰撞&#xff0c;将剩余的石头重新放入堆中。 C 算法代码&#xff1a; class Solution …

中兴云电脑W101D2-晶晨S905L3A-2G+8G-安卓9-线刷固件包

中兴云电脑W101D2-晶晨S905L3A-2G8G-WIFI-蓝牙5.0-3个USB2.0-线刷包线刷方法&#xff1a;1、准备好一根双公头USB线刷刷机线&#xff0c;长度30-50CM长度最佳&#xff0c;同时准备一台电脑&#xff1b;2、电脑上安装好刷机工具Amlogic USB Burning Tool 软件 →打开软件 →文件…

Android OkHttp 底层原理和实战完全教程(责任链模式详解)

目录 1. OkHttp 入门:从一个请求开始 1.1 基本 GET 请求:三步走 1.2 同步 vs 异步:选择你的风格 1.3 为什么选 OkHttp? 2. 配置 OkHttpClient:打造你的专属网络引擎 2.1 超时设置:别让请求卡死 2.2 添加拦截器:窥探请求全过程 2.3 缓存:让请求更快更省流量 3. …

【RK3588部署yolo】算法篇

简历描述收集并制作军事伪装目标数据集&#xff0c;包含真实与伪装各种类型军事目标共计60余类。其中&#xff0c;包含最新战场充气伪装军事装备30余类&#xff0c;并为每一张图片制作了详细的标注。针对军事伪装目标的特点&#xff0c;在YOLOv8的Backbone与Neck部分分别加…

【Spring Boot 快速入门】一、入门

目录Spring Boot 简介Web 入门Spring Boot 快速入门HTTP 协议概述请求协议响应协议解析协议TomcatSpring Boot 简介 Spring Boot 是由 Pivotal 团队&#xff08;后被 VMware 收购&#xff09;开发的基于 Spring 框架的开源项目&#xff0c;于 2014 年首次发布。其核心目标是简…

如何调整服务器的内核参数?-哈尔滨云前沿

调整服务器内核参数是一项较为专业的操作&#xff0c;不同的操作系统调整方式略有不同&#xff0c;以下以常见的 Linux 系统为例&#xff0c;介绍一些调整服务器内核参数的一般步骤和常用参数&#xff1a;一般步骤 备份当前配置&#xff1a;在修改内核参数之前&#xff0c;先备…

C++基础:模拟实现queue和stack。底层:适配器

引言模拟实现queue和stack&#xff0c;理解适配器&#xff0c;实现起来非常简单。一、适配器 适配器是一种能让原本不兼容的接口协同工作的设计模式或者组件。它的主要作用是对一个类的接口进行转换&#xff0c;使其符合另一个类的期望接口&#xff0c;进而实现适配和复用。&am…

OI 杂题

OI 杂题字符串括号匹配例 1&#xff1a;与之前的类似&#xff0c;就是讲一点技巧&#xff0c;但是比较乱&#xff0c;凑合着看吧。 字符串 括号匹配 几何意义&#xff1a;考虑令 ( 为 111 变换&#xff0c;令 ) 为 −1-1−1 变换&#xff0c;然后对这个 1/−11/-11/−1 构成…

【论文阅读】Safety Alignment Should Be Made More Than Just a Few Tokens Deep

Safety Alignment Should Be Made More Than Just a Few Tokens Deep原文摘要问题提出现状与漏洞&#xff1a;当前LLMs的安全对齐机制容易被攻破&#xff0c;即使是简单的攻击&#xff08;如对抗性后缀攻击&#xff09;或良性的微调也可能导致模型越狱。核心论点&#xff1a; 作…

Generative AI in Game Development

如有侵权或其他问题&#xff0c;欢迎留言联系更正或删除。 出处&#xff1a;CHI 20241. 一段话总结本研究通过对来自 Reddit 和 Facebook 群组的 3,091 条独立游戏开发者的在线帖子和评论进行定性分析&#xff0c;探讨了他们对生成式 AI在游戏开发中多方面作用的认知与设想。研…

【C++算法】72.队列+宽搜_二叉树的最大宽度

文章目录题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;题目链接&#xff1a; 662. 二叉树最大宽度 题目描述&#xff1a; 解法 这里的宽度指的是一层的最右边的非空节点到一层的最左边的非空节点&#xff0c;一共的节点数。 解法一&#xff1a;硬来&am…

什么是3DVR?VR技术有哪些应用场景?

VR与3D技术解析及应用在高科技领域&#xff0c;VR和3D是两个常被提及的名词。那么&#xff0c;这两者之间究竟存在着怎样的区别与联系呢&#xff1f;简而来说&#xff0c;VR技术是3D技术的一种高级延展和深化应用。3D技术&#xff0c;即将二维设计图转化为立体、逼真的视觉效果…

栈与队列:数据结构核心解密

栈和队列的基本 栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。元素的插入和删除操作只能在栈顶进行。常见的操作包括压栈(push)和弹栈(pop)。 队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构。元素的插入在队尾进行,删除在队…

《C++初阶之STL》【list容器:详解 + 实现】

【list容器&#xff1a;详解 实现】目录前言------------标准接口介绍------------标准模板库中的list容器是什么样的呢&#xff1f;1. 常见的构造2. 迭代器操作std::list::beginstd::list::endstd::list::rbeginstd::list::rend3. 容量的操作std::list::sizestd::list::empty…

【灰度实验】——图像预处理(OpenCV)

目录 1 灰度图 2 最大值法 3 平均值法 4 加权均值法 5 两个极端的灰度值 将彩色图转为灰度图地过程称为灰度化。 灰度图是单通道图像&#xff0c;灰度化本质就是将彩色图的三通道合并成一个通道的过程。三种合并方法&#xff1a;最大值法&#xff0c;平均值法和加权均值法…

【linux驱动开发】编译linux驱动程序报错:ERROR: Kernel configuration is invalid.

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、报错二、解决方法1.先编译linux内核源码2.再重新编译驱动程序一、报错 在编译驱动程序过程中&#xff0c;经常碰到的一个小问题&#xff1a; make -C /home/lu…

Java面试宝典:MySQL中的锁

InnoDB中锁的类型非常多,总体上可以如下分类: 这些锁都是做什么的?具体含义是什么?我们现在来一一学习。 1. 解决并发事务问题 我们已经知道事务并发执行时可能带来的各种问题。最大的一个难点是:一方面要最大程度地利用数据库的并发访问能力,另一方面又要确保每个用户…

设备识别最佳实践:四维交叉验证框架

设备识别最佳实践&#xff1a;四维交叉验证框架 1. MAC地址分析&#xff08;40%权重&#xff09; - 设备身份核验 核心方法&#xff1a; # MAC地址标准化&#xff08;OUI提取&#xff09; mac"B4:2E:99:FB:9D:78" oui$(echo $mac | tr -d : | cut -c 1-6 | tr a-f A-…

《Java 程序设计》第 9 章 - 内部类、枚举和注解

大家好&#xff0c;今天我们来学习《Java 程序设计》第 9 章的内容 —— 内部类、枚举和注解。这三个知识点是 Java 中提升代码灵活性和可读性的重要工具&#xff0c;在实际开发中非常常用。接下来我们逐一展开讲解&#xff0c;每个知识点都会配上可直接运行的代码示例&#xf…