背景
软件系统中,往往存在一些周期性任务需要反复执行,以下是一些常见的业务需求:
- 单据流水号按日/月/年重置:单据流水号往往出于业务含义考虑,会包含一部分日期+若干位流水号,需要在新的一天、新的一个月、新的一年起始的时候,将初始值重置为1,如HZ202500001。
- 定时邮件通知:设定周期性任务,发送定期更新、提醒或营销邮件。
- 库存管理:定期检查库存水平,并触发补货或促销活动。
- 定期生成统计图表:通过调度任务,定期收集数据并生成所需的统计图表,包括日报、周报、月报等
- 自动解锁用户:出于安全考虑,用户连续多次输入错误密码后会自动锁定用户一段时间,如10分钟,以防止恶意用户暴力破解密码,对于这部分锁定用户,需要调度任务来自动解锁。
- 每天限量使用功能的可用量恢复:如图片文字识别OCR、语音转文字等功能,限制用户每天使用10次,需要调度任务来重置可用量。
- 数据备份和恢复:定期执行数据备份任务,确保在数据丢失或损坏时能够快速恢复。
对于以上功能需求,平台应提供统一、灵活的调度任务功能来满足。
功能组件
在Java领域中,有多种任务调度功能组件可以帮助开发者实现定时任务和周期性任务的调度。
以下是一些常用的调度框架和库:
- Java自带的
Timer
和TimerTask
类:- Java标准库中的
Timer
类可以用来调度TimerTask
任务,但它不是线程安全的,并且功能有限。
- Java标准库中的
ScheduledExecutorService
接口:- 来自
java.util.concurrent
包的ScheduledExecutorService
提供了更强大的线程池和调度功能。
- 来自
- Spring Framework的
@Scheduled
注解:- Spring框架提供了
@Scheduled
注解,使得在Spring应用中配置和执行定时任务变得简单。
- Spring框架提供了
- JCronTab:
- JCronTab是一个轻量级的定时任务调度库,它提供了类似于Unix的cron工具的功能。
- EasyScheduling:
- EasyScheduling是当当网开源的一个定时任务调度框架,提供了丰富的调度策略。
- Quartz Scheduler:
- Quartz是一个开源的作业调度库,功能强大,可以配置复杂的调度策略,并且易于集成。
- Elastic-Job:
- Elastic-Job是一个分布式调度解决方案,由阿里巴巴开源,用于处理大规模分布式任务。
- xxl-job:
- XXL-JOB是一个轻量级、分布式的任务调度框架,由雪球科技开源。
在选择调度组件时,需要考虑任务的复杂性、系统的分布式需求、易用性和社区支持等因素。
对于简单的定时任务,可以使用Java自带的Timer
类或Spring的@Scheduled
注解;而对于复杂的、分布式的任务调度,则可以考虑使用Quartz或Elastic-Job等更专业的调度框架。
在此推荐单体架构使用Quartz,微服务架构则看规模,规模大使用Elastic-Job,规模小使用xxl-job。
集成Quartz
一二三应用开发平台集成的是Quartz功能组件实现的调度任务,接下来就来说一下整体设计、集成工作及注意事项。
创建模块
首先,出于职责单一的考虑,为任务调度集成单设了一个模块,命名为platform-boot-starter-scheduler,依赖于通用模块,在整体架构图中位置如下:
模块内部结构如下:
添加依赖
在模块的pom文件中,增加组件依赖,如下:
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
组件配置
因为是基于spring-boot-starter-quartz的二次封装,因此配置放在了spring的节点下。
需要注意的是,配置放在本模块下application-platform.yml中并不会生效,需要放在了开发平台的platform-boot-starter模块的application-platform.yml中。
如下所示:
#数据连接
spring:
quartz:
scan: true
jdbc:
# 初始化Quartz表结构,项目第一次启动配置成always,然后改成never 否则已生成的job会被初始化掉
initialize-schema: never
#设置quartz任务的数据持久化方式,默认是内存方式
job-store-type: jdbc
properties:
org:
quartz:
scheduler:
#可以为任意字符串,对于scheduler来说此值没有意义,但是可以区分同一系统中多个不同的实例,
#如果使用了集群的功能,就必须对每一个实例使用相同的名称,这样使这些实例“逻辑上”是同一个scheduler。
instanceName: MyScheduler
#默认主机名和时间戳生成实例ID,可以是任何字符串,但对于所有调度程序来说,必须是唯一的
instanceId: AUTO
jobStore:
#持久化配置
#application自己管理事务
class: org.quartz.impl.jdbcjobstore.JobStoreTX
#类似于Hibernate的dialect,用于处理DB之间的差异,StdJDBCDelegate能满足大部分的DB
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#以指示JDBCJobStore将JobDataMaps中的所有值都作为字符串,因此可以作为名称 - 值对存储而不是在BLOB列中
#以其序列化形式存储更多复杂的对象。从长远来看,这是更安全的,因为您避免了将非String类序列化为BLOB的类版本问题。
useProperties: true
#数据库表前缀
tablePrefix: qrtz_
#最大能忍受的触发超时时间,如果超时则认为“失误”(以毫秒为单位)
misfireThreshold: 60000
#是否是应用在集群中,当应用在集群中时必须设置为TRUE,否则会出错。
#如果有多个Quartz实例在用同一套数据库时,必须设置为true。
isClustered: true
#设置此实例“检入”与群集的其他实例的频率(以毫秒为单位)。影响检测失败实例的速度。
clusterCheckinInterval: 5000
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 30
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
主要配置项都做了详细的注释,不展开赘述。
注意initialize-schema用于初始化Quartz表结构,项目第一次启动配置成always,然后改成never 否则已生成的job会被初始化掉。
功能设计
Quartz组件可以实现任务调度功能,但是仅限于代码层面的调用,并没有可视化界面来辅助配。作为开发平台,为了更方便地进行配置,通过集成进行二次封装功能,实现了任务配置和调度任务配置功能的可视化。
任务配置功能用于定义具体“干活”的任务类。
调度任务配置用于调度“干活”的任务类,即为任务设定执行周期,并控制其暂停、恢复、执行等。
任务配置
使用平台的低代码配置功能,配置实体属性如下:
同时,有些任务需要传入参数,因此配置了一个辅助的任务参数类,如下:
以自动解锁账号为例,输入任务名称、对应的执行类全路径、状态、排序、备注信息。
其中任务类,需要继承Quartz框架的QuartzJobBean类,重写executeInternal方法,源码如下:
package tech.abc.platform.system.scheduler;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import tech.abc.platform.common.annotation.SystemLog;
import tech.abc.platform.common.enums.LogTypeEnum;
import tech.abc.platform.system.constant.SystemConstant;
import tech.abc.platform.system.entity.User;
import tech.abc.platform.common.enums.ExecuteResultEnum;
import tech.abc.platform.system.enums.UserStatusEnum;
import tech.abc.platform.system.service.ParamService;
import tech.abc.platform.system.service.UserService;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
/**
* 自动解锁账号
*
* @author wqliu
* @date 2023-05-24
*/
@Component
@Slf4j
@DisallowConcurrentExecution
public class AutoUnlockAccount extends QuartzJobBean {
@Autowired
private UserService userService;
@Autowired
private ParamService paramService;
@Override
@SystemLog(value = "解锁用户", logType = LogTypeEnum.SCHEDULER, logRequestParam = false, executeResult = ExecuteResultEnum.SUCCESS)
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 获取所有锁定的账户
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.lambda().eq(User::getStatus, UserStatusEnum.LOCK.toString());
List<User> userList = userService.list(userQueryWrapper);
if (userList != null && !userList.isEmpty()) {
int lockTime = Integer.parseInt(paramService.getParamValue(SystemConstant.ACCOUNT_UNLOCK_INTERVAL));
for (User user : userList) {
long intervalMin = Duration.between(user.getLockTime(), LocalDateTime.now()).toMinutes();
if (lockTime <= intervalMin) {
// 自动解锁用户
userService.unlock(user.getId());
}
}
}
}
}
调度任务配置
使用平台的低代码配置功能,配置实体属性如下:
同时,有些任务需要传入参数,因此配置了一个辅助的调度任务参数类,来适配任务类的任务参数。
以自动解锁账号调度任务为例,选择上一节配置的任务,设置触发器,以及状态、排序、备注信息。
通过封装vue组件,实现了cron表达式的可视化配置,调度触发设置界面如下:
集成部分
调度任务最终要发挥作用,需要调用Quartz的API,此部分我们定义了一个服务接口,源码如下:
package tech.abc.platform.scheduler.service;
import java.util.Map;
/**
* 任务调度引擎服务定义
*
* @author wqliu
* @date 2023-05-24
*/
public interface SchedulerService {
/**
* 新增任务
*
* @param jobClassName 任务类名
* @param schedulerJobId 任务标识
* @param cronExpression cron表达式
* @param parameters 参数
* @param schedulerJobName 调度任务名
*/
void add(String jobClassName, String schedulerJobId, String cronExpression,
Map<String, String> parameters,
String schedulerJobName);
/**
* 移除任务
*
* @param id 任务标识
*/
void remove(String id);
/**
* 修改任务
*
* @param jobClassName 任务类名
* @param schedulerJobId 任务标识
* @param cronExpression cron表达式
* @param parameters 参数
* @param schedulerJobName 调度任务名
*/
void modify(String jobClassName, String schedulerJobId, String cronExpression,
Map<String, String> parameters,
String schedulerJobName);
/**
* 暂停任务
*
* @param id 任务标识
*/
void pause(String id);
/**
* 恢复任务
*
* @param id 任务标识
*/
void resume(String id);
/**
* 暂停所有任务
*/
void pauseAll();
/**
* 恢复所有任务
*/
void resumeAll();
/**
* 执行任务
*
* @param id 任务标识
*/
void execute(String id);
}
服务实现调用了Quartz框架的Scheduler类,源码如下:
package tech.abc.platform.scheduler.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.abc.platform.common.exception.CommonException;
import tech.abc.platform.common.exception.CustomException;
import tech.abc.platform.scheduler.exception.SchedulerExceptionEnum;
import tech.abc.platform.scheduler.service.SchedulerService;
import java.util.Map;
/**
* quartz调度任务实现类
*
* @author wqliu
* @date 2023-05-24
*/
@Service
@Slf4j
public class SchedulerServiceImpl implements SchedulerService {
@Autowired
private Scheduler scheduler;
@Override
public void add(String jobClassName, String schedulerJobId, String cronExpression,
Map<String, String> parameters,
String schedulerJobName) {
// 构建job信息
JobDataMap jobDataMap = new JobDataMap(parameters);
JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(schedulerJobId)
.usingJobData(jobDataMap)
.withDescription(schedulerJobName)
.build();
// 表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression)
// 错过的触发合并为一次执行
.withMisfireHandlingInstructionFireAndProceed();
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(schedulerJobId)
.withDescription(schedulerJobName)
.withSchedule(scheduleBuilder)
.build();
try {
// 添加任务
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.REGISTER_SCHEDULER_JOB);
}
}
@Override
public void modify(String jobClassName, String schedulerJobId, String cronExpression,
Map<String, String> parameters, String schedulerJobName) {
// 删除任务
remove(schedulerJobId);
// 构建job信息
add(jobClassName, schedulerJobId, cronExpression, parameters, schedulerJobName);
}
@Override
public void remove(String id) {
try {
scheduler.pauseTrigger(TriggerKey.triggerKey(id));
scheduler.unscheduleJob(TriggerKey.triggerKey(id));
scheduler.deleteJob(JobKey.jobKey(id));
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.SCHEDULER_JOB_DELETE_FAILURE);
}
}
@Override
public void pause(String id) {
try {
scheduler.pauseJob(JobKey.jobKey(id));
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.SCHEDULER_JOB_PAUSE_FAILURE);
}
}
@Override
public void resume(String id) {
try {
scheduler.resumeJob(JobKey.jobKey(id));
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.SCHEDULER_JOB_RESUME_FAILURE);
}
}
@Override
public void pauseAll() {
try {
scheduler.pauseAll();
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.SCHEDULER_JOB_PAUSE_FAILURE);
}
}
@Override
public void resumeAll() {
try {
scheduler.resumeAll();
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.SCHEDULER_JOB_RESUME_FAILURE);
}
}
@Override
public void execute(String id) {
try {
scheduler.triggerJob(JobKey.jobKey(id));
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
throw new CustomException(SchedulerExceptionEnum.EXECUTE_ERROR);
}
}
private static Job getClass(String className) {
try {
Class<?> jobClass = Class.forName(className);
return (Job) jobClass.newInstance();
} catch (Exception ex) {
log.error("未找到任务类", ex);
throw new CustomException(CommonException.CLASS_NOT_FOUND);
}
}
}
小结
通过上述集成工作,一二三开发平台实现了依托Quartz实现调度任务的功能。
当有周期性任务需求时,在开发层面,需要继承Quartz框架的QuartzJobBean类,重写executeInternal方法,然后通过平台的任务调度功能模块,在平台中注册,并设置触发周期,从而实现灵活配置、便于调整的目的。
可以对于同一个任务,通过传入不同的参数,设置不同的周期,达成不同的目的,进一步提升了复用性和灵活性。
开源平台资料
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:[csdn专栏]
开源地址:[Gitee]
开源协议:MIT
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!