> 文章列表 > 你知道怎么实现定时任务吗?

你知道怎么实现定时任务吗?

你知道怎么实现定时任务吗?

诸位读者都知道笔者写东西都是用到才写,笔者的学习足迹自从参加工作之后就是 非系统 学习了,公司里源代码只要有笔者不知道的技术细节,笔者就会仔细的研究清楚,笔者是不喜欢给自己留下问题的那种学习习惯。

为何要写

笔者最近负责了消息发送的一些业务需求,由于笔者工作年限不到一年,且笔者目前只是普通本科大四学生,技术栈并不是很完善(crud程序员只是起点),例如国内很多公司都在用的许雪里大神开发的xxl-job计划任务框架,tk-mybaytis框架(笔者也不知道公司为什么不直接用mybatis而是选用了它的其他版本),自封装的ORM框架(用法与mybatis不一样的地方在于它需要自己写Sql语句进行封装),以及各种自封装的自研框架。由于接触了需求,定时任务发送消息提醒用户。所以笔者不得不学习测试一下定时任务框架来加深更多的了解,不能在开发过程中知其然不知其所以然。

定时任务的实现方式

说到头,定时任务就是类似于将cron语句: 秒 分 时 日 月 周 年  交给linux系统进行执行,到了指定的时间就得执行这些命令,在业务需求里,我们需要系统去定时催促实现某些事情,其实这种场景非常多,要写程序实现计时的功能我觉得读者们应该都有自己的方式:例如 利用java线程sleep的方式,利用 timerTak的工具包,利用redis键值对过期消息回调,利用前端计时器,利用spring框架自带的注解@Scheduled (计划的)搭配cron语句,利用数据库event实现,利用xxl-job框架实现,利用quartz框架实现等等......

之前笔者就分享了一套用java线程池实现的定时任务工具,底层就是sleep方法,缺陷太多,不适合生产环境,所以为了加深对公司项目中xxl-job业务代码的理解,笔者研究了quartz框架实现计划任务的原理与实现方式,记录下来供笔者日后进一步深研。

下面笔者举例部分代码供读者阅读:

1、java线程sleep实现计时任务:

 2、java工具包TimerTask实现定时任务:

 3、利用redis键值对过期回调:

4、前端js函数setTimeOut(函数,毫秒)也可定时;

5、 spring注解@Scheduled实现定时任务:

6、利用数据库事件event实现:

事件调度器: Event Scheduler可以用做定时执行某些特定任务(例如:删除记录、数据统计报告、数据备份等等),来取代原先只能由操作系统的计划任务来执行的工作(可以精确到秒)区分表级别的触发器Tigger(事件触发的任务是周期时间决定,而触发器是由表级动作决定的)。

(1)开启数据库事件调度:

SHOW VARIABLES LIKE 'event%';  表中的event_scheduler值为on即为已经开启。

开启:SET GLOBAL event_scheduler = 1;SET GLOBAL event_scheduler = ON;
关闭:SET GLOBAL event_scheduler = 0;SET GLOBAL event_scheduler = OFF;

如果想持久化配置项可以将event_scheduler=1写入到数据库配置文件my.cnf文件(区别my.inf文件)中。

(2)创建事件调度:

CREATE EVENT 【调度事件名称】
ON SCHEDULE 【指定好调度时间】
DO 【SQL可执行语句】

具体细节以及其他语法读者们请自行www.baidu,com;

(3)举例实现调度语句:

CREATE EVENT delete_db_and_run
ON SCHEDULE  AT  TIMESTEP '2023-04-16 16:00:00'
DO  DELETE FROM alldata

以上就是在2023年4月16日16点整删除数据表alldata的定时计划语法

Quartz框架的技术细节与底层原理

笔者总结的技术点若有任何问题,欢迎读者指出,阅即改正。

技术细节:Quartz是通过数据库持久化来实现计划的数据保留,但它的原理并不是基于数据库event的方式进行实现的计划任务。它可以扩展更多的需求也业务,你可以自定义事件只要它们继承了Job接口。Quartz优质的地方在于它与xxl-job一样是基于数据库进行事件调度的,这样就算是server宕机了也不影响我们定时任务的存储,只要在容忍时间内重启服务器我们的定时任务还是可以正常执行的。

底层原理:我们只需要搞清楚两个问题即可明白该框架的主流程脉络,至于其他的枝叶逻辑笔者并没有深究,这两个主流程问题就是该框架是如何将任务存入数据库的?该框架是如何获取定时任务的时间并判断给出动作的?

问题1:quartz框架是如何将任务存储进它的配置数据表中的?

笔者还是以源码跟踪的方式与读者们一起研究这里面的主要脉络:

首先在官方demo中我们能发现至关重要的语句,也是查找源码的起始点:

 org.quartz.Scheduler类实现的调度任务方法,就是添加计划任务的主要实现代码,笔者点进源码发现以下方法链调用:

 

 (触发器同理也能找到类似于更新与插入方法的调用,我们先揪着一个走就行)

 这里其实已经很清晰了,运用了JDBC采用动态注入的方式进行数据库的更新与插入操作。

其实quartz还有其他数据库类型可以使用,但是入库的主流程脉络大差不差,我们首先要知道它就是通过我们crud程序员最擅长的方式进行的数据库存储定时任务信息的。

问题2:quartz是如何获取定时任务的时间并判断给出动作的?

关于这个问题,笔者查看了依赖包的层级结构,找到了core中的核心QuartzSchedulerThread类;

 显然这就是quartz做出动作的核心线程类,关于它做了什么,我们只需要看线程的run方法即可:

由于此类的run方法行数比较长,笔者将分开叙述它做了什么:

首先在while中优先给出了同步持有对象锁sigLock,通先查询halted的原子布尔型变量的判断,如果停止的值为false则进入等待阶段等到之后进行下一次扫描:

 下一步判断线程池可用线程数,可用数大于0后获取将要触发的触发器链表Tiggers:

 注意方法 this.qsRsrcs.getJobStore().acquireNextTriggers 是获取将要触发的触发器列表:

这里笔者直接将获取将要触发的触发器列表方法核心逻辑展示出来供读者阅读: 

之后就是 判断时间有没有到该触发的时间,因为触发器列表中的触发器都是将要触发的,判断到快要触发了。所以quartz这里做了等待,如果trigger的nextFireTime比当前时间大2ms则循环等待。timeUntilTrigger就是nextFireTime和当前时间之间的差值,它在循环中不断更新,直到它的值非常小了,之后才继续向下到真正的触发代码:

 然后就是触发了:

 我们继续看触发方法tiggersFired()的部分核心代码:

 之后的逻辑方法就是数据库操作,记录操作:

 将库修改之后就要进行脚本的执行了(这里需要注意的是执行是线程的异步操作,并行执行,但也有一定的控制逻辑),根据数据库的触发结果获取脚本shell:

 拿到shell之后就可以到线程池中丢过去执行了:

 实现了Runnable接口的shell当然可以直接执行了,最后我们发现它对于未获取到将要触发的触发器列表时,它会自动等待:

 开始获取锁等待是为了定期执行,后面的获取锁等待是为了在等最近可以触发的触发器列表。

到这里笔者和读者们大概也明白了quartz的获取判断与触发逻辑,理解了第二大主流程脉络,当然里面的逻辑肯定不止这些,但是笔者认为理解了主流程脉络结合实践加深吃入程度即可。

 Quartz框架的实现demo实践

笔者也做了一个小demo实现我们的计划任务业务需求,当然仅仅供读者们阅读与学习,不支持生产使用。

依赖:

        <!-- quartz依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId><version>2.5.4</version></dependency>

quartz.sql

#1 保存已经触发的触发器状态信息
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
#2 存放暂停掉的触发器表表
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
#3 调度器状态表
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
#4 存储程序的悲观锁的信息(假如使用了悲观锁)
DROP TABLE IF EXISTS QRTZ_LOCKS;
#5 简单的触发器表
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
#6 存储两种类型的触发器表
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
#7 定时触发器表
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
#8 以blob 类型存储的触发器
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
#9 触发器表
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
#10 job 详细信息表
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
#11 日历信息表
DROP TABLE IF EXISTS QRTZ_CALENDARS;#job 详细信息表
CREATE TABLE QRTZ_JOB_DETAILS
(SCHED_NAME VARCHAR(120) NOT NULL,JOB_NAME  VARCHAR(200) NOT NULL,JOB_GROUP VARCHAR(200) NOT NULL,DESCRIPTION VARCHAR(250) NULL,JOB_CLASS_NAME   VARCHAR(250) NOT NULL,IS_DURABLE VARCHAR(1) NOT NULL,IS_NONCONCURRENT VARCHAR(1) NOT NULL,IS_UPDATE_DATA VARCHAR(1) NOT NULL,REQUESTS_RECOVERY VARCHAR(1) NOT NULL,JOB_DATA BLOB NULL,PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);CREATE TABLE QRTZ_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,JOB_NAME  VARCHAR(200) NOT NULL,JOB_GROUP VARCHAR(200) NOT NULL,DESCRIPTION VARCHAR(250) NULL,NEXT_FIRE_TIME BIGINT(13) NULL,PREV_FIRE_TIME BIGINT(13) NULL,PRIORITY INTEGER NULL,TRIGGER_STATE VARCHAR(16) NOT NULL,TRIGGER_TYPE VARCHAR(8) NOT NULL,START_TIME BIGINT(13) NOT NULL,END_TIME BIGINT(13) NULL,CALENDAR_NAME VARCHAR(200) NULL,MISFIRE_INSTR SMALLINT(2) NULL,JOB_DATA BLOB NULL,PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,REPEAT_COUNT BIGINT(7) NOT NULL,REPEAT_INTERVAL BIGINT(12) NOT NULL,TIMES_TRIGGERED BIGINT(10) NOT NULL,PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);CREATE TABLE QRTZ_CRON_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,CRON_EXPRESSION VARCHAR(200) NOT NULL,TIME_ZONE_ID VARCHAR(80),PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,STR_PROP_1 VARCHAR(512) NULL,STR_PROP_2 VARCHAR(512) NULL,STR_PROP_3 VARCHAR(512) NULL,INT_PROP_1 INT NULL,INT_PROP_2 INT NULL,LONG_PROP_1 BIGINT NULL,LONG_PROP_2 BIGINT NULL,DEC_PROP_1 NUMERIC(13,4) NULL,DEC_PROP_2 NUMERIC(13,4) NULL,BOOL_PROP_1 VARCHAR(1) NULL,BOOL_PROP_2 VARCHAR(1) NULL,PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);CREATE TABLE QRTZ_BLOB_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,BLOB_DATA BLOB NULL,PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);CREATE TABLE QRTZ_CALENDARS
(SCHED_NAME VARCHAR(120) NOT NULL,CALENDAR_NAME  VARCHAR(200) NOT NULL,CALENDAR BLOB NOT NULL,PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(SCHED_NAME VARCHAR(120) NOT NULL,TRIGGER_GROUP  VARCHAR(200) NOT NULL,PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);CREATE TABLE QRTZ_FIRED_TRIGGERS
(SCHED_NAME VARCHAR(120) NOT NULL,ENTRY_ID VARCHAR(95) NOT NULL,TRIGGER_NAME VARCHAR(200) NOT NULL,TRIGGER_GROUP VARCHAR(200) NOT NULL,INSTANCE_NAME VARCHAR(200) NOT NULL,FIRED_TIME BIGINT(13) NOT NULL,SCHED_TIME BIGINT(13) NOT NULL,PRIORITY INTEGER NOT NULL,STATE VARCHAR(16) NOT NULL,JOB_NAME VARCHAR(200) NULL,JOB_GROUP VARCHAR(200) NULL,IS_NONCONCURRENT VARCHAR(1) NULL,REQUESTS_RECOVERY VARCHAR(1) NULL,PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);CREATE TABLE QRTZ_SCHEDULER_STATE
(SCHED_NAME VARCHAR(120) NOT NULL,INSTANCE_NAME VARCHAR(200) NOT NULL,LAST_CHECKIN_TIME BIGINT(13) NOT NULL,CHECKIN_INTERVAL BIGINT(13) NOT NULL,PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);CREATE TABLE QRTZ_LOCKS
(SCHED_NAME VARCHAR(120) NOT NULL,LOCK_NAME  VARCHAR(40) NOT NULL,PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

配置文件application.yml

server:port: 9000
spring:application:name: quartz-servicedatasource:type: com.zaxxer.hikari.HikariDataSourceurl: jdbc:mysql://localhost:3306/your quartz db ?useUnicode=true&characterEncoding=utf8username: your db usernamepassword: your db passworddriver-class-name: com.mysql.cj.jdbc.Driverquartz:# 相关属性配置properties:org:quartz:# 数据源dataSource:globalJobDataSource:# URL必须大写URL: jdbc:mysql://127.0.0.1:3306/your quartz db ?useUnicode=true&characterEncoding=utf-8&useSSL=falsedriver: com.mysql.cj.jdbc.DrivermaxConnections: 5username: your db usernamepassword: your db password # 必须指定数据源类型provider: hikaricpscheduler:instanceName: globalScheduler# 实例id#instanceId: AUTOtype: com.alibaba.druid.pool.DruidDataSourcejobStore:# 数据源dataSource: globalJobDataSource# JobStoreTX将用于独立环境,提交和回滚都将由这个类处理class: org.quartz.impl.jdbcjobstore.JobStoreTX# 驱动配置driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate# 表前缀tablePrefix: QRTZ_# 线程池配置threadPool:class: org.quartz.simpl.SimpleThreadPool# 线程数threadCount: 10# 优先级threadPriority: 5

实体类

@Data
public class JobInfo {private String jobName;private String jobGroup;private String triggerName;private String triggerGroup;private String cron;private String className;private String status;private String nextTime;private String prevTime;private String config;
}
@Data
public class Message {private String title;private String message;private String jobGroup;private String jobName;private String sendUserName;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private String sendDate;
}

 核心任务处理类

@Component
public class JobHandler {@Resourceprivate Scheduler scheduler;public void addJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {Assert.notNull(jobInfo, LocalDateTime.now().toString() + "-> 任务信息为null拒绝生成定时任务");JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());if (!scheduler.checkExists(jobKey)) {Class<Job> jobClass = (Class<Job>) Class.forName(jobInfo.getClassName());JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobKey).withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()).withIdentity(jobInfo.getJobName()).build();jobDetail.getJobDataMap().put("config", jobInfo.getConfig());TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup());Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCron())).build();scheduler.scheduleJob(jobDetail,trigger);} else {throw new SchedulerException(jobInfo.getJobName() + "->定时任务计划库已存在");}}public void pauseJob(String jobGroup, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName, jobGroup);if (scheduler.checkExists(jobKey)) {scheduler.pauseJob(jobKey);}}public void resumeJob(String jobGroup, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName, jobGroup);if (scheduler.checkExists(jobKey)) {scheduler.resumeJob(jobKey);}}public Boolean deleteJob(String jobGroup, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName, jobGroup);if (scheduler.checkExists(jobKey)) {return scheduler.deleteJob(jobKey);}return false;}public JobInfo getJobInfo(String jobGroup, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName,jobGroup);if (!scheduler.checkExists(jobKey)){return null;}List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(jobKey);Assert.notNull(triggersOfJob, LocalDateTime.now().toString()+"->触发器信息为空->"+jobGroup+"/"+jobName);TriggerKey key = triggersOfJob.get(0).getKey();Trigger.TriggerState state = scheduler.getTriggerState(key);JobDetail jobDetail = scheduler.getJobDetail(jobKey);JobInfo jobInfo = new JobInfo();jobInfo.setJobGroup(jobGroup);jobInfo.setJobName(jobName);jobInfo.setTriggerGroup(key.getGroup());jobInfo.setJobName(key.getName());jobInfo.setClassName(jobDetail.getJobClass().getName());jobInfo.setStatus(state.toString());if (Objects.nonNull(jobDetail.getJobDataMap())){jobInfo.setConfig(JSONObject.toJSONString(jobDetail.getJobDataMap()));}CronTrigger trigger = (CronTrigger) triggersOfJob.get(0);jobInfo.setCron(trigger.getCronExpression());return jobInfo;}
}

事件类型

@Component
@DisallowConcurrentExecution
public class PlanRemindJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {System.out.println("执行计划任务" + jobExecutionContext.getJobDetail().getDescription());}
}
@Component
@DisallowConcurrentExecution
public class TimeEventJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {System.out.println("执行定时任务" + jobExecutionContext.getJobDetail().getDescription());}
}

事件类型枚举

public enum JobType {//计划任务与定时任务PLAN_REMIND_MESSAGE ,TIME_EVENT_MESSAGE 
}

业务接口

public interface QuartzService {Boolean insertMessage(Message message, JobType jobType) throws SchedulerException, ClassNotFoundException;JobInfo getJob(Message message);Boolean deleteJob(Message message);Boolean resumeJob(Message message);
}

 业务实现类

@Service("QuartzService")
public class QuartzServiceImpl implements QuartzService {@Resourceprivate JobHandler jobHandler;@Overridepublic synchronized Boolean insertMessage(Message message, JobType jobType) {JobInfo jobInfo = JobGenerator.getJobInfo(message, jobType);try {jobHandler.addJob(jobInfo);return true;} catch (Exception e) {e.printStackTrace();return false;}}@Overridepublic JobInfo getJob(Message message) {try {return jobHandler.getJobInfo(message.getJobGroup(), message.getJobName());} catch (SchedulerException e) {e.printStackTrace();return null;}}@Overridepublic synchronized Boolean deleteJob(Message message) {try {return jobHandler.deleteJob(message.getJobGroup(), message.getJobName());} catch (SchedulerException e) {e.printStackTrace();return false;}}@Overridepublic synchronized Boolean resumeJob(Message message) {try {jobHandler.resumeJob(message.getJobGroup(), message.getJobName());return true;} catch (SchedulerException e) {e.printStackTrace();return false;}}
}

工具类

cron与日期(“yyyy-MM-dd hh:mm:ss”)转换类

public class CronGenerator {public static String geCron(Message message){String date = message.getSendDate().toString();String yyyy = date.substring(0, 4);String MM = date.substring(5, 7);String dd = date.substring(8, 10);String hh = date.substring(11, 13);String mm = date.substring(14, 16);String ss = date.substring(17, 19);return ss+" "+mm+" "+hh+" "+dd+" "+MM+" "+"? "+yyyy;}
}

JobInfo实体转换工具类

public class JobGenerator {public static JobInfo getJobInfo(Message message, JobType jobType) {String cron = CronGenerator.geCron(message);JobInfo jobInfo = new JobInfo();jobInfo.setCron(cron);if (jobType == JobType.PLAN_REMIND_MESSAGE){jobInfo.setClassName("com.hlc.quartzservice.jobType.PlanRemindJob");jobInfo.setJobGroup(message.getJobGroup());jobInfo.setJobName(message.getJobName());jobInfo.setTriggerGroup("PLAN_REMIND_MESSAGE");jobInfo.setTriggerName("计划任务触发器");jobInfo.setConfig(message.getMessage());}else if (jobType == JobType.TIME_EVENT_MESSAGE){jobInfo.setClassName("com.hlc.quartzservice.jobType.TimeEventJob");jobInfo.setJobGroup(message.getJobGroup());jobInfo.setJobName(message.getJobName());jobInfo.setTriggerGroup("TIME_EVENT_MESSAGE");jobInfo.setTriggerName("定时任务触发器");jobInfo.setConfig(message.getMessage());}return jobInfo;}
}

开启应用程序进行测试

@SpringBootApplication
public class QuartzServiceApplication {public static void main(String[] args) {SpringApplication.run(QuartzServiceApplication.class, args);QuartzService quartzService = SpringContextHolder.getBean(QuartzService.class);Message message = new Message();message.setMessage("测试");message.setSendDate("2023-04-15 14:30:00");message.setTitle("消息");message.setSendUserName("hlc");try {quartzService.insertMessage(message, JobType.PLAN_REMIND_MESSAGE);} catch (SchedulerException | ClassNotFoundException e) {e.printStackTrace();}}
}

静态方法获取bean的获取容器上下文工具类(实现ApplicationContextAware接口)

@Component
@Lazy(value = false)
public class SpringContextHolder implements ApplicationContextAware {/*** 将上下文静态设置,在初始化组件时就进行静态上下文的覆盖(这个覆盖是将远spring容器的上下文对象引用加到我们预定设置)*/private static ApplicationContext applicationContext = null;public static ApplicationContext getApplicationContext() {assertContextInjected();return applicationContext;}@SuppressWarnings("unchecked")public static <T> T getBean(String name) {assertContextInjected();return (T) applicationContext.getBean(name);}public static  <T> T getBean(Class<T> beanType) {assertContextInjected();return applicationContext.getBean(beanType);}@Overridepublic void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {SpringContextHolder.applicationContext = applicationContext;}public void destroy() {applicationContext = null;}private static void assertContextInjected() {Assert.notNull(applicationContext,"applicationContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.");}public static void pushEvent(ApplicationEvent event){assertContextInjected();applicationContext.publishEvent(event);}}

还有另外一种方法可以测试,就是查库,看看数据库表中有没有发生数据变化?

给的么详细,确定不点点赞,收藏一下吗?