赞
踩
《从零打造项目》系列文章
工具
ORM框架选型
数据库变更管理
定时任务框架
- Java定时任务技术分析
- SpringBoot结合Quartz实现定时任务
缓存
- 待更新
安全框架
- 待更新
开发规范
- 待更新
假设我们有这样两个需求:
1、用户注册1分钟后给用户发送欢迎通知。
2、每天8点钟给用户发送当天温度通知。
接下来我们就准备实现上述两个需求,关于通知发送就只是简单地控制台输出,没有真正实现该功能。
关于定时任务框架的选择,本文将选用 Quartz 来实现上述需求,下面简单介绍一下 Quartz。
Quartz 作为一个优秀的开源调度框架,Quartz 具有以下特点:
另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。
在 Quartz 体系结构中,有三个组件非常重要:
Quartz 的存储方式有两种:RAMJobStore 和 JDBCJobStore。从名字就能看出,存在内存中和存在数据库当中。在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。
两者之间的区别如下图所示:
JDBCJobStore 存储可以实现 Quartz 集群模式,实际场景下,我们必然需要考虑定时任务的高可用,即选用集群模式。
Quartz 集群架构如下,集群中的每个节点是一个独立的 Quartz 应用,且独立的 Quartz 节点并不与另一节点通信,而是通过相同的数据库表来感知另一 Quartz 应用。简而言之,Quartz 应用、数据库支撑、多节点部署即可搭建起Quartz的应用集群。
**Quartz 集群共用同一个数据库,由数据库中的数据来确定任务是否正在执行,如果该任务正在执行,则其他服务器就不能去执行该调度任务。**Quartz集群的特点如下:
1、持久化
Quartz 可以将调度器 scheduler、触发器 trigger 以及任务 Job 的运行时信息存储至数据库中,采用 JDBCJobStore,如果服务器异常时,可以基于数据库中的存储信息进行任务恢复。
2、高可用性
如果相关服务器节点挂掉的话,集群的其他节点则会继续执行相关任务。
3、伸缩性
如果集群中的节点数过少,导致相关任务无法及时执行,可以增加额外的服务器节点,只需将其他节点上的脚本及配置信息拷贝至新部署的节点上运行即可。
4、负载均衡
Quartz 使用随机的负载均衡算法,任务 job 是以随机的方式由不同的节点上 Scheduler 实例来执行。但当前不存在一个方法指派一个Job到集群中的特定节点。
下面我们就使用 Quartz 来实现定时任务推送。
创建一个 Maven 项目,名为 quartz-task。
1、在 pom.xml
文件中,引入相关依赖。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <fastjson.version>1.2.73</fastjson.version> <hutool.version>5.5.1</hutool.version> <mysql.version>8.0.19</mysql.version> <org.mapstruct.version>1.4.2.Final</org.mapstruct.version> <org.projectlombok.version>1.18.20</org.projectlombok.version> <druid.version>1.1.18</druid.version> <springdoc.version>1.6.9</springdoc.version> <liquibase.version>4.16.1</liquibase.version> </properties> <dependencies> <!-- 实现对 Spring MVC 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 实现对 Quartz 的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>4.16.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.12</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>${springdoc.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.liquibase</groupId> <artifactId>liquibase-maven-plugin</artifactId> <version>4.16.1</version> <configuration> <!--properties文件路径,该文件记录了数据库连接信息等--> <propertyFile>src/main/resources/application.yml</propertyFile> <propertyFileWillOverride>true</propertyFileWillOverride> </configuration> </plugin> <plugin> <groupId>com.msdn.hresh</groupId> <artifactId>liquibase-changelog-generate</artifactId> <version>1.0-SNAPSHOT</version> <configuration> <sourceFolderPath>src/main/resources/liquibase/changelogs/ </sourceFolderPath><!-- 当前应用根目录 --> </configuration> </plugin> </plugins> </build>
2、添加 application.yml
server: port: 8080 spring: application: name: quartz-task datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/quartz_test_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false username: root password: root liquibase: enabled: true change-log: classpath:liquibase/master.xml # 记录版本日志表 database-change-log-table: databasechangelog # 记录版本改变lock表 database-change-log-lock-table: databasechangeloglock quartz: # 程序结束时会等待quartz相关的内容结束 wait-for-jobs-to-complete-on-shutdown: true # 将任务等保存化到数据库 job-store-type: jdbc # QuartzScheduler启动时覆盖己存在的Job overwrite-existing-jobs: false mybatis: mapper-locations: classpath:mapper/*Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl lazy-loading-enabled: true changeLogFile: src/main/resources/liquibase/master.xml
3、关于 Quartz 的配置,可以一并写在 application.yml 中,类似于这样:
spring: datasource: user: url: jdbc:mysql://127.0.0.1:3306/quartz_test_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: quartz: url: jdbc:mysql://127.0.0.1:3306/quartz_test_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: # Quartz 的配置,对应 QuartzProperties 配置类 quartz: scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置 properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 org: quartz: # JobStore 相关配置 jobStore: # 数据源名称 dataSource: quartzDataSource # 使用的数据源 class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate tablePrefix: QRTZ_ # Quartz 表前缀 isClustered: true # 是集群模式 clusterCheckinInterval: 1000 useProperties: false # 线程池相关配置 threadPool: threadCount: 25 # 线程池大小。默认为 10 。 threadPriority: 5 # 线程优先级 class: org.quartz.simpl.SimpleThreadPool # 线程池类型
不过因为 Quartz 配置内容过多,所以单独新建了 quartz.properties。
org.quartz.jobStore.useProperties=true #在集群中每个实例都必须有一个唯一的instanceId,但是应该有一个相同的instanceName【默认“QuartzScheduler”】【非必须】 org.quartz.scheduler.instanceName=quartzScheduler # Scheduler实例ID,全局唯一 org.quartz.scheduler.instanceId=AUTO # 指定scheduler的主线程是否为后台线程,【默认false】【非必须】 org.quartz.scheduler.makeSchedulerThreadDaemon=true # 触发job时是否需要拥有锁 org.quartz.jobStore.acquireTriggersWithinLock = true #线程池配置 #线程池类型 org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool #线程池大小 org.quartz.threadPool.threadCount=10 #线程优先级 org.quartz.threadPool.threadPriority=5 #============================================================================ # Configure JobStore #============================================================================ org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.tablePrefix=qrtz_ # 最大能忍受的触发超时时间(触发器被认定为“misfired”之前),如果超过则认为“失误”【默认60秒】 org.quartz.jobStore.misfireThreshold = 60000 # 配置数据源的名称,在后面配置数据源的时候要用到, # 例如org.quartz.dataSource.myDS.driver=com.mysql.cj.jdbc.Driver org.quartz.jobStore.dataSource = myDS # 集群配置 org.quartz.jobStore.isClustered = true # 检入到数据库中的频率(毫秒)。检查是否其他的实例到了应当检入的时候未检入这能指出一个失败的实例, # 且当前Scheduler会以此来接管执行失败并可恢复的Job通过检入操作,Scheduler也会更新自身的状态记录 org.quartz.jobStore.clusterCheckinInterval=5000 # jobStore处理未按时触发的Job的数量 org.quartz.jobStore.maxMisfiresToHandleAtATime=20 # datasource org.quartz.dataSource.myDS.provider = hikaricp org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.dataSource.myDS.driver=com.mysql.cj.jdbc.Driver org.quartz.dataSource.myDS.URL=jdbc:mysql://localhost:3306/quartz_test_db?characterEncoding=utf8 org.quartz.dataSource.myDS.user=root org.quartz.dataSource.myDS.password=root # 最大连接数 org.quartz.dataSource.myDS.maxConnections = 10 # dataSource用于检测connection是否failed/corrupt的SQL语句 org.quartz.dataSource.myDS.validationQuery = select 1
关于 properties 文件中每个属性的含义,推荐阅读《Quartz配置文件详解&生产配置》。
4、手动在数据库中创建 Quartz 相关表,可以从 Quartz 发行版下载中找到tables_mysql.sql
,或直接从其源代码中找到 。因为我们使用 MySQL ,所以使用 tables_mysql_innodb.sql
脚本。
1、Quartz 配置类
@Configuration
public class SchedulerConfig {
@Bean
public SchedulerFactoryBean scheduler(DataSource dataSource) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
schedulerFactory.setDataSource(dataSource);
schedulerFactory.setJobFactory(new SpringBeanJobFactory());
schedulerFactory.setApplicationContextSchedulerContextKey("applicationContext");
return schedulerFactory;
}
}
2、Quartz 相关实体类
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class ScheduleTask { // 任务名 private String jobName; // 任务组 private String groupName; // 任务数据 private String jobData; // 任务执行处理类,小写字母开头 private String jobHandlerClass; // 任务执行时间 private Long jobTime; // 任务执行时间,cron时间表达式 (如:0/5 * * * * ? ) private String jobCronTime; // 任务执行次数,(<0:表示不限次数) private int jobTimes; } @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class JobResponse { // 任务名 private String jobName; // 任务组 private String groupName; // 任务数据 private String jobData; private String triggerKey; private String jobStatus; // 任务执行时间,cron时间表达式 (如:0/5 * * * * ? ) private String jobCronTime; }
3、QuartzService定时器操作
@Service @RequiredArgsConstructor public class QuartzTaskService { public static final String JOB_DATA_KEY = "jobData"; public static final String JOB_HANDLER_CLASS_KEY = "jobHandlerClass"; private final Scheduler scheduler; public void createJob(ScheduleTask task) throws SchedulerException { JobDetail jobDetail = JobBuilder.newJob().ofType(MessageJob.class) .withIdentity(task.getJobName(), task.getGroupName()) .usingJobData(JOB_DATA_KEY, task.getJobData()) .usingJobData(JOB_HANDLER_CLASS_KEY, task.getJobHandlerClass()) .build(); Trigger trigger; if (StrUtil.isNotBlank(task.getJobCronTime())) { trigger = TriggerBuilder.newTrigger().forJob(jobDetail) .withIdentity(task.getJobName() + "_trigger", task.getGroupName()) .withSchedule(CronScheduleBuilder.cronSchedule(task.getJobCronTime())).build(); } else { trigger = TriggerBuilder.newTrigger().forJob(jobDetail) .withIdentity(task.getJobName() + "_trigger", task.getGroupName()) .startAt(new Date(task.getJobTime())) .build(); } scheduler.scheduleJob(jobDetail, trigger); } // 修改 一个job的 时间表达式 @SneakyThrows public void updateJob(String jobName, String jobGroupName, String jobTime) { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); trigger = trigger.getTriggerBuilder().withIdentity(triggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).build(); // 重启触发器 scheduler.rescheduleJob(triggerKey, trigger); } @SneakyThrows public void removeTask(JobKey jobKey) { scheduler.deleteJob(jobKey); } // 暂停一个job @SneakyThrows public void pauseJob(JobKey jobKey) { scheduler.pauseJob(jobKey); } // 恢复一个job @SneakyThrows public void resumeJob(JobKey jobKey) { scheduler.resumeJob(jobKey); } // 立即执行一个job @SneakyThrows public void runJobNow(JobKey jobKey) { scheduler.triggerJob(jobKey); } // 获取所有计划中的任务列表 public List<JobResponse> queryAllJob() throws SchedulerException { List<JobResponse> jobResponses = new ArrayList<>(); GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup(); Set<JobKey> jobKeys = scheduler.getJobKeys(matcher); for (JobKey jobKey : jobKeys) { List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); for (Trigger trigger : triggers) { Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); JobResponse jobResponse = getJobResponse(jobKey, trigger, triggerState); jobResponses.add(jobResponse); } } return jobResponses; } private JobResponse getJobResponse(JobKey jobKey, Trigger trigger, TriggerState triggerState) { JobResponse jobResponse = JobResponse.builder().jobName(jobKey.getName()) .groupName(jobKey.getGroup()).triggerKey(trigger.getKey().toString()).build(); jobResponse.setJobStatus(triggerState.name()); if (trigger instanceof CronTrigger) { CronTrigger cronTrigger = (CronTrigger) trigger; String cronExpression = cronTrigger.getCronExpression(); jobResponse.setJobCronTime(cronExpression); } return jobResponse; } // 获取所有正在运行的job public List<JobResponse> queryRunJob() throws SchedulerException { List<JobResponse> jobResponses = new ArrayList<>(); List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs(); for (JobExecutionContext executingJob : executingJobs) { JobDetail jobDetail = executingJob.getJobDetail(); JobKey jobKey = jobDetail.getKey(); Trigger trigger = executingJob.getTrigger(); Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); JobResponse jobResponse = getJobResponse(jobKey, trigger, triggerState); jobResponses.add(jobResponse); } return jobResponses; } }
4、自定义 Job 类
@Setter @Slf4j public class MessageJob implements Job { private ApplicationContext applicationContext; private String jobData; private String jobHandlerClass; @SneakyThrows @Override public void execute(JobExecutionContext context) { log.info("quartz job data: " + jobData + ", jobHandlerClass: " + jobHandlerClass); MessageHandler messageHandler = (MessageHandler) applicationContext.getBean(jobHandlerClass); messageHandler.handlerMessage(jobData); } }
5、创建定时任务处理接口
public interface MessageHandler {
void handlerMessage(String jobData) throws JsonProcessingException;
}
不同的定时任务对应不同的任务处理类,即实现 MessageHandler 接口。
1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。
@Service @RequiredArgsConstructor @Slf4j public class UserService { private final ScheduleTaskService scheduleTaskService; private final UserMapper userMapper; private final UserStruct userStruct; private final WeatherService weatherService; /** * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知. * * @param userRequest 用户请求体 */ public void register(UserRequest userRequest) { if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) || isBlank(userRequest.getPassword())) { BusinessException.fail("账号或密码为空!"); } User user = userStruct.toUser(userRequest); userMapper.insert(user); scheduleTaskService.createTask(user.getUsername()); } public void sayHelloToUser(String username) { User user = userMapper.selectByUserName(username); String message = "Welcome to Java,I am hresh."; log.info(user.getUsername() + " , hello, " + message); } public void pushWeatherNotification(List<User> users) { log.info("执行发送天气通知给用户的任务。。。"); WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN); for (User user : users) { log.info(user.getUsername() + "----" + weatherInfo.toString()); } } }
2、ScheduleTaskService 创建定时任务
@Service @RequiredArgsConstructor public class ScheduleTaskService { private final QuartzTaskService quartzTaskService; private final UserMapper userMapper; @SneakyThrows public void createTask(String username) { LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L); String jobName = "sayHello"; String jogGroupName = "group1"; String jobHandlerClass = "sayHelloHandler"; ScheduleTask scheduleTask = ScheduleTask.builder() .jobName(jobName) .groupName(jogGroupName) .jobData(new ObjectMapper().writeValueAsString(username)) .jobHandlerClass(jobHandlerClass) .jobTime(DateUtil.toEpochMilli(scheduleTime)) .build(); quartzTaskService.createJob(scheduleTask); } @SneakyThrows public void createWeatherNotificationTask(String jobTime) { String jobName = "weatherNotification"; String jogGroupName = "group2"; String jobHandlerClass = "weatherNotificationHandler"; List<User> users = userMapper.queryAll(); ScheduleTask scheduleTask = ScheduleTask.builder() .jobName(jobName) .groupName(jogGroupName) .jobData(JSON.toJSONString(users)) .jobHandlerClass(jobHandlerClass) .jobCronTime(jobTime) .build(); quartzTaskService.createJob(scheduleTask); } }
3、WeatherService,获取天气温度等信息
@Service @RequiredArgsConstructor public class WeatherService { private final RestTemplate restTemplate; public WeatherInfo getWeather(WeatherConstant weatherConstant) { String json = restTemplate .getForObject("http://t.weather.sojson.com/api/weather/city/" + weatherConstant.getCode() , String.class); JSONObject jsonObject = JSONObject.parseObject(json); Integer status = jsonObject.getInteger("status"); String currentDay = DateUtil.getDay(LocalDateTime.now()); if (status == 200) { JSONObject data = jsonObject.getJSONObject("data"); String quality = data.getString("quality"); String notice = data.getString("ganmao"); String currentTemperature = data.getString("wendu"); JSONArray forecast = data.getJSONArray("forecast"); JSONObject dayInfo = forecast.getJSONObject(0); String high = dayInfo.getString("high"); String low = dayInfo.getString("low"); String weather = dayInfo.getString("type"); String windDirection = dayInfo.getString("fx"); return WeatherInfo.builder().airQuality(quality + "污染").date(currentDay) .cityName(weatherConstant.getCityName()).temperature(low + "-" + high).weather(weather) .windDirection(windDirection).notice(notice).currentTemperature(currentTemperature) .build(); } return null; } }
4、UserController,对外暴露接口
@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; private final ScheduleTaskService scheduleTaskService; @PostMapping("/register") public Result<Object> register(@RequestBody UserRequest userRequest) { userService.register(userRequest); return Result.ok(); } @PostMapping("/weather-notification") public Result<Object> scheduledSayHello(@RequestParam("jobTime") String jobTime) { scheduleTaskService.createWeatherNotificationTask(jobTime); return Result.ok(); } }
还有一些代码没有展示出来,感兴趣的朋友到时候可以去我的 Github 上看一下项目源码。
为了演示效果,发送天气温度通知,我们暂时设为每2分钟一次。
首先通过 postman 来注册用户
可以到数据库中看一下 qrtz_job_details 表中的数据,如下所示:
等待一分钟后,控制台会输出如下内容:
执行完定时任务后,qrtz_job_details 表中相关数据也会被删除掉。
接着来测试发送天气通知
因为咱们测试的是每两分钟跑一次定时任务,所以 qrtz_job_details 表中会一直存在这么一条数据:
1、初次启动定时任务报错
Couldn't acquire next trigger: Unknown column 'SCHED_TIME' in 'field list'
原因:我们下载的 SQL 文件有问题,在 qrtz_fired_triggers 表的构建语句中缺少 sched_time 字段,完整的 SQL 语句如下:
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) );
2、增加数据源配置过程中遇到的坑:
在 quartz.properties 文件中没有增加 org.quartz.dataSource.myDS.provider = hikaricp
配置时,启动一直报错:
Caused by: org.quartz.SchedulerException: Could not initialize DataSource: qzDS
Caused by: org.quartz.SchedulerException: ConnectionProvider class ‘org.quartz.utils.C3p0PoolingConnectionProvider’ could not be instantiated.
后来增加了 provider: hikaricp 这个配置,启动不报错。
Quartz 框架出现的比较早,后续不少定时框架,或多或少都基于 Quartz 研发的,比如当当网的elastic-job
就是基于quartz
二次开发之后的分布式调度解决方案。
并且,Quartz
并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。
虽然 Quartz 可以实现我们的需求,但代码入侵比较严重,使用起来比较麻烦,后续我们再研究一下其他的定时任务框架。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
Quartz.NET Configuration Reference
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。