1、实现使用quartz提供的默认11张持久化表存储quartz相关信息。
2、实现定时任务的编辑、启动、关闭、删除。
3、实现自定义持久化表存储quartz定时任务信息。
4、本案例使用springboot整合mybatis框架和MySQL数据库实现持久化
5、提供源码下载
可以将调度信息存储到数据库中,进行持久化,当程序中断,再次启动的时候。任然会保留中断之前的数据,继续执行。
1、Quartz中通过JobStore来存储任务和触发器信息,Quartz默认使用RAMJobstore将任务和触发器的信息存储在内存中,但是当服务器宕机内存中的信息会丢失。
2、Quartz中通过JDBCJobStore将任务和触发器等信息保存在数据库中,实现持久化。
3、JDBCJobStoreSupport中包含两个子类:
JobStoreTX:表示自己管理事务,存储在数据库中,当程序中断,调度信息不会丢失,
支持事务,支持集群,再次启动时,会恢复因程序关闭,重启而错过的任务。
JobStoreCMT:表示使用容器管理事务
针对不同的数据库,表就放在不同的sql文件中。
如果你使用的是mysql数据库表信息就在tables_mysql.sql文件中。
重点:这些表不会自动执行,需要拷贝出来手动创建在你的数据库中。
mysql表信息如下
QRTZ _JOB_DETAILS 存储每一个已配置的Job的详细信息
QRTZ _TRIGGERS????? 存储已配置的Trigger的信息
QRTZ_BLOB_TRIGGERS??? Trigger作为Blob类型存储
QRTZ _SIMPLE_TRIGGERS???存储SimpleTrigger的信息,包括重复次数、间隔、以及已触的次数
QRTZ _CRON_TRIGGERS? 存储CronTrigger,包括Cron表达式和时区信息
QRTZ _SIMPROP_TRIGGERS存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种触发器
QRTZ _CALENDARS? 存储Quartz的Calendar信息
QRTZ _PAUSED_TRIGGER_GRPS??????存储已暂停的Trigger组的信息
QRTZ _FIRED_TRIGGERS?? 存储与已触发的Trigger相关的状态信息,以及相关Job的执行信息
QRTZ _SCHEDULER_STATE????存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ _LOCKS?? 存储程序的悲观锁的信息
创表信息如下:
#
# Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar
#
# PLEASE consider using mysql with innodb tables to avoid locking issues
#
# In your Quartz properties file, you'll need to set
# org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
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)
);
commit;
配置文件位置:
配置参考文档:
https://www.w3cschool.cn/quartz_doc/quartz_doc-i7oc2d9l.html
在正式开始之前我们需要明确一点,quartz自带的表数据的添加功能是quartz源码中自带的,我们只需要正确的配置数据源即可自动的添加数据。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>quartzpersistencedemo3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quartzpersistencedemo3</name>
<description>quartzpersistencedemo3</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
最重要的配置就是连接数据库及job-store-type: jdbc,其他的配置如果不需要都可以不写。
最重要的配置就是连接数据库及job-store-type: jdbc,其他的配置如果不需要都可以不写。
spring:
datasource:
url: jdbc:mysql://localhost:3306/quartz?characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
quartz:
# 任务存储类型
job-store-type: jdbc
# 关闭时等待任务完成
wait-for-jobs-to-complete-on-shutdown: false
# 是否覆盖已有的任务
overwrite-existing-jobs: true
# 是否自动启动计划程序
auto-startup: true
# 延迟启动
startup-delay: 0s
jdbc:
# 数据库架构初始化模式(never:从不进行初始化;always:每次都清空数据库进行初始化;embedded:只初始化内存数据库(默认值))
# 注意:第一次启动后,需要将always改为never,否则后续每次启动都会重新初始化quartz数据库
initialize-schema: never
# 用于初始化数据库架构的SQL文件的路径
# schema: classpath:sql/tables_mysql_innodb.sql
# 相关属性配置
properties:
org:
quartz:
scheduler:
# 调度器实例名称
instanceName: QuartzScheduler
# 分布式节点ID自动生成
instanceId: AUTO
jobStore:
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 表前缀
tablePrefix: QRTZ_
# 是否开启集群
isClustered: true
# 数据源别名(自定义)
dataSource: quartz
# 分布式节点有效性检查时间间隔(毫秒)
clusterCheckinInterval: 10000
useProperties: false
# 线程池配置
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
在配置类中配置JobDetail和trigger监听器
这个配置类只是添加任务的快捷方式,这中方式在下面的添加任务中会被替代。
@Configuration
public class QuartzJobConfig {
@Bean
public JobDetail jobDetail(){
JobDetail detail= JobBuilder.newJob(MyQuartzJob.class)
.withIdentity("job33","group33")
.storeDurably()//设置Job持久化
//设置job数据
.usingJobData("username","xiaochun2")
.usingJobData("useraddr","安徽合肥2")
.build();
return detail;
}
//创建触发器,触发实例
@Bean
public Trigger trigger(){
//每隔5秒执行一次
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
Trigger trigger= TriggerBuilder.newTrigger()
.forJob(jobDetail())
.withIdentity("trigger33","group33")
.withSchedule(cronScheduleBuilder)
.startNow()
.build();
return trigger;
}
}
@Slf4j
public class MyQuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//执行的具体内容
log.info("=========quartzpersistencedemo3--quartz具体内容============");
System.out.println(context.getJobInstance());
System.out.println(context.getJobDetail().getJobDataMap().get("username"));
}
}
从图中可以看出,任务已经顺利的启动
【qrtz_cron_triggers表数据】
【qrtz_job_details表数据】
【qrtz_triggers表数据】
【qrtz_scheduler_state表数据】
说明1:通过scheduler.pauseJob暂停任务。
说明2:scheduler.pauseJob需要的两个参数是qrtz_job_details表中的JOB_NAME和JOB_GROUP字段的值。
说明3:本案例中只涉及到任务本身的暂停,考虑到业务的并发和分布式的情况,在暂停前可以判断一下任务是否存在,如果还创建了自己的业务表,应该在任务暂停后修改自己的表状态。个人业务表的所有操作按照常规操作即可。
@Controller
public class QuartzController {
@Resource
Scheduler scheduler;
//暂停任务
@RequestMapping("/suspendJob")
@ResponseBody
public String suspendJob(){
String jobName="job33";
String jobGroup="group33";
try{
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
return "暂停成功";
}catch (Exception e){
return "暂停失败";
}
}
}
重点说明:这个时候重启项目,程序会自动的加载QuartzConfig并创建JobDetail和Trigger并自动的向数据库添加数据。
核心添加的qrtz_cron_triggers、qrtz_job_details、qrtz_triggers、qrtz_scheduler_state
说明1:通过scheduler.resumeJob重启任务
说明2:scheduler. resumeJob需要的两个参数是qrtz_job_details表中的JOB_NAME和JOB_GROUP字段的值。
@Controller
public class QuartzController {
@Resource
Scheduler scheduler;
//重启任务
@RequestMapping("/resumeJob")
@ResponseBody
public String resumeJob() {
String jobName="job33";
String jobGroup="group33";
try {
//恢复任务
scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
return "重启成功";
} catch (SchedulerException e) {
return "重启失败";
}
}
}
删除任务会清除数据库中的数据
@Controller
public class QuartzController {
@Resource
Scheduler scheduler;
//删除任务
@RequestMapping("/removeJob")
@ResponseBody
public String removeJob() throws SchedulerException {
String jobName="job33";
String jobGroup="group33";
//先暂停任务
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
//获取任务触发器
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
try {
//停止触发器
scheduler.pauseTrigger(triggerKey);
//移除触发器
scheduler.unscheduleJob(triggerKey);
//删除任务
scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup));
return "删除任务成功";
} catch (SchedulerException e) {
return "删除任务失败";
}
}
}
重点:只会启动一次
@Controller
public class QuartzController {
@Resource
Scheduler scheduler;
//立即启动任务
@RequestMapping("/triggerJob")
@ResponseBody
public String triggerJob() {
String jobName="job33";
String jobGroup="group33";
try {
scheduler.triggerJob(JobKey.jobKey(jobName,jobGroup));
return "启动成功";
} catch (SchedulerException e) {
return "启动失败";
}
}
}
【添加任务依赖对象】
对时区不了解,可以看这篇文章:Java中ZonedDateTime使用详解及时间转化(java中获取时区)_java zoneddatetime-CSDN博客
对时区不了解,可以看这篇文章:Java中ZonedDateTime使用详解及时间转化(java中获取时区)_java zoneddatetime-CSDN博客
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class JobInfo {
private String jobName;//job名称
private String jobGroup;//job所属组
private String triggerName;//触发器名称
private String jobDescription;//job任务描述
private Map<String,Object> userData;//job携带的用户参数
private String cron;//触发器规则
private String timeZoneId;//当前时区
}
在项目中使用到了ObjectMapper对象,将map转化成json字符串,不了解可以看文章:
Springboot中解析JSON字符串(jackson库ObjectMapper解析JSON字符串)-CSDN博客
重点:小伙伴需要注意了,创建任务的时候MyQuartzJob任务本身这个是需要提前创建的。
//添加任务
@RequestMapping(value = "/addJob",method = RequestMethod.POST)
@ResponseBody
public String addJob(@RequestBody JobInfo jobInfo) throws JsonProcessingException {
System.out.println("======addJob==="+jobInfo.toString());
//通过jobKey判断任务是否唯一
// jobKey有jobName和jobGroup组成
JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());
try {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
if (Objects.nonNull(jobDetail)) {
scheduler.deleteJob(jobKey);
}
} catch (SchedulerException e) {
e.printStackTrace();
}
//将map转化成json的工具
ObjectMapper objectMapper=new ObjectMapper();
//任务详情
JobDetail jobDetail = JobBuilder.newJob(MyQuartzJob.class)
//jobDetail描述
.withDescription(jobInfo.getJobDescription()) //任务描述
.usingJobData("userData",objectMapper.writeValueAsString(jobInfo.getUserData()))
.withIdentity(jobKey) //指定任务
.build();
//根据cron,TimeZone时区,指定执行计划
CronScheduleBuilder builder =
CronScheduleBuilder
//任务表达式
.cronSchedule(jobInfo.getCron())
.inTimeZone(TimeZone.getTimeZone(jobInfo.getTimeZoneId()));
//触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobInfo.getTriggerName(), jobInfo.getJobGroup()).startNow()
.withSchedule(builder)
.build();
//添加任务
try {
scheduler.scheduleJob(jobDetail, trigger);
return "任务添加成功";
} catch (SchedulerException e) {
System.out.println(e.getMessage());
}
return "任务添加失败";
}
由于我们是在body中传递的数据,在addJob方法接受的参数需要使用@RequestBody注解,
如果不使用请求获取不到body的raw传递的数据。
测试参数:
{
? "jobName":"jobName1",
? "jobGroup":"jobGroup1",
? "triggerName":"triggerName1",
? "jobDescription":"三点、六点、九点发优惠卷。",
? "cron":"0/5 * * * * ?",
? "timeZoneId":"Asia/Shanghai",
? "userData":{"name":"123"}
}
数据库参数:
结果输出:
修改任务的本质就是添加任务,只要保证jobName,jobGroup,triggerName等关键参数不变,即可修改数据。
Quartz总共提供了11张表来持久化分布式任务调度的相关信息,总体来说功能强大,但是比较冗余。这个时候很多人希望自己创建一张简单的表,实现任务管理也是可以的。但是这个时候对自定义表的增删改查操作都需要自己写,而无法使用默认提供了功能。
创建信息可以参照如下:
CREATE TABLE `my_job` (
? `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
? `jobName` varchar(50) NOT NULL COMMENT '任务名称',
? `jobGroupName` varchar(50) NOT NULL COMMENT '任务组名',??`jobTriggerName` varchar(50) NOT NULL COMMENT '触发器名称',
? `jobCron` varchar(50) NOT NULL COMMENT '时间规则表达式',
? `jobClassPath` varchar(200) NOT NULL COMMENT '类全类型',
? `jobDataMap` varchar(100) DEFAULT NULL COMMENT '用户数据',
? `jobStatus` int(2) NOT NULL COMMENT '任务状态',
? `jobDescribe` varchar(100) DEFAULT NULL COMMENT '任务功能描述',
? PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
值得注意的是,如果这个时候通过自定义修改了任务状态为体质状态(如停止状态为1),修改之后还需要调用scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));,任他任务也是。因为我们的停止本质只是修改数据库的值。
?
8、源码下载