在开发过程中经常遇到需要定时处理的一些任务, 例如每天的消费提醒、打卡等等. 简单的操作时直接在代码中写一个周期循环的方法, 例如:工作日每天上午10点整提醒用户打卡,
但是这样做有个缺陷, 我们并没有办法去控制该任务调度的开启和关闭, 以下代码提供一种可控制的定时任务调度.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "remind_info")
@TableName("remind_info")
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class RemindInfoEntity extends BaseTenantEntity {
private static final long serialVersionUID = -8763550312275818392L;
/**
* 提醒类型
* */
@Column(columnDefinition = "varchar(64) COMMENT '提醒类型'")
private String remindType;
/**
* 提醒内容内容
* */
@Column(columnDefinition = "text COMMENT '提醒内容内容'")
private String remindContent;
/**
* 时间表达式,格式:0 * * * * ? (每分钟的00秒)
* */
@Column(columnDefinition = "varchar(100) COMMENT '时间表达式, 格式:0 * * * * ? (每分钟的00秒)'")
private String cron;
/**
* 启用状态,0-关闭,1-启动
* */
@Column(columnDefinition = "tinyint(0) COMMENT '启用状态,0-关闭,1-启动'")
private int status;
}
@Autowired
private RemindInfoService remindInfoService;
@Autowired
private TaskScheduledService raskScheduledService;
/**
* 通过id启动定时任务
* @Transactional ——启用事务回滚,避免在发生异常时产生脏数据或错误的数据操作
* */
@RequestMapping("/start")
@Transactional
public String start(Long id) {
try {
/**
* 首先更新提醒任务表中的任务状态
* 此处的状态0或1可以用枚举类进行替代或者说明
* */
int i = remindInfoService.updateStatusById(id, 1);
//当提醒任务信息存在时,再来创建定时任务
if(i != 0) {
raskScheduledService.start(id);
return "启动成功";
}else {
return "当前任务不存在";
}
} catch (Exception e) {
//主动回滚事务,因为在检查型异常中,事务回滚不生效
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return "启动异常,请联系管理员进行处理";
}
@RequestMapping("/stop")
@Transactional
public String stop(Long id) {
try {
/**
* 首先更新提醒任务表中的任务状态
* 此处的状态0或1可以用枚举类进行替代或者说明
* */
int i = remindInfoService.updateStatusById(id, 0);
if(i != 0) {
raskScheduledService.stop(id);
return "停止成功";
}else {
return "当前任务不存在";
}
} catch (Exception e) {
//主动回滚事务,因为在检查型异常中,事务回滚不生效
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
return "停止失败,请联系管理员";
}
/**
* 提醒信息业务逻辑层
*
* 注:mybatis-plus可以 extends ServiceImpl,然后使用service层相关的单表增删查改方法
* */
@Service
public class RemindInfoService extends BaseServiceAdapter<RemindInfoMapper,RemindInfoEntity> {
@Autowired
private RemindInfoMapper remindInfoMapper;
/**
* @apiNote 条件获取列表
* */
public List<RemindInfoEntity> getList(Map<String,Object> param) {
LambdaQueryWrapper<RemindInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.isNotBlank((String)param.get("remindType")),RemindInfoEntity::getRemindType,param.get("remindType"));
queryWrapper.eq(StringUtils.isNotBlank(param.get("status")+""),RemindInfoEntity::getStatus,param.get("status"));
return remindInfoMapper.selectList(queryWrapper);
}
/**
* @apiNote 通过id更新状态
* @param id —— 主键
* status —— 只能传0(关闭)或1(启动)
* */
public int updateStatusById(Long id ,Integer status) {
RemindInfoEntity remindInfoEntity =getById(id);
if (remindInfoEntity != null ) {
remindInfoEntity.setStatus(status);
return remindInfoMapper.updateById(remindInfoEntity);
}
return 0;
}
}
/**
* 用于获取上文实例对象
* 该工具在项目中是用于在多线程中获取spring中相关的bean对象来调用对应的方法
* */
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
private static ApplicationContext applicationContext = null;
/**
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
/**
* 获取静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 从静态变量applicationContext中得到Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中得到Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}
/**
* 任务定时程序线程类
* 用于实现线程的执行逻辑
* */
@Getter
@Setter
public class TaskScheduledThread implements Runnable{
private static final Logger LOGGER = LoggerFactory.getLogger(TaskScheduledThread.class);
/**
* 由于该类并没有交由spring进行管理,通过@Autowired标注时会出错,因此通过上下文工具对象来获取对应的实例对象
* */
public static final RemindInfoService remindInfoService = SpringContextUtils.getBean(RemindInfoService.class);
/* 任务主键 */
private Long taskId;
/* 任务内容 */
private String taskContent;
/* 任务类型 */
private String taskTyle;
@Override
public void run() {
//判断当前线程对象的类型
//此处逻辑原来是提醒业务相关的,现在统一修改为控制台打印信息
switch (this.taskTyle) {
//根据不同的操作类型,实现不同的操作逻辑
case "类型1":
//执行相关逻辑1
LOGGER.info("当前定时任务为:{}任务 , 调度内容为:{}",this.taskTyle,this.taskContent);
break;
case "类型2":
//执行相关逻辑2
LOGGER.info("当前定时任务为:{}任务 , 调度内容为:{}",this.taskTyle,this.taskContent);
break;
/* 。。。。。。。*/
default:
LOGGER.info("当前定时任务类型为异常:{},请联系管理员",this.taskTyle);
break;
}
LOGGER.info("remindInfoService对象地址为:{}", JSONArray.toJSON(remindInfoService.getById(this.taskId)));
}
}
/**
* 定时任务业务逻辑层
* */
@Service
public class TaskScheduledService {
public static final String 定时锁="dingshi";
@Resource
private RedisUtil<String> redisUtil;
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(TaskScheduledService.class);
/**
* @description 定时任务线程池
*/
@Resource
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
/**
* 存放已经启动的任务map,此处使用ConcurrentHashMap进行存储,确保在不同线程下启动任务存放值时产生线程不安全的问题
* 存放形式:K-任务id ,V- ScheduledFuture对象
* 作用:统一管理整个系统的定时任务,可以根据任务id来获取定时任务对象,从而进行检查、启动和关闭等操作
*/
private Map<Long, ScheduledFuture> scheduledFutureMap = new ConcurrentHashMap<>();
@Autowired
private RemindInfoService remindInfoService;
/**
* @apiNote 根据任务id 启动定时任务(对外开放的启动接口,可以通过对象进行访问)
*
*/
public void start(Long id) {
LOGGER.info("准备启动任务:{}", id);
/*
* 此处添加锁,为了确保只有一个线程在执行以下逻辑,防止多人启动多次
* */
boolean lock = redisUtil.lock(定时锁, 10);
try {
if(lock) {
RemindInfoEntity remindInfo = remindInfoService.getById(id);
//校验是否已经启动
if (this.isStart(id)) {
LOGGER.info("当前任务已在启动列表,请不要重复启动!");
} else {
//启动任务
this.doStart(remindInfo);
}
}
}catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
redisUtil.deleteLock(定时锁);
}
}
/**
* 根据任务id 判断定时任务是否启动
*/
public Boolean isStart(Long id) {
//首先检查scheduledFutureMap是否存在该任务,如果不存在,则确定当前任务并没有启动
if (scheduledFutureMap.containsKey(id)) {
//当该任务存在时,需要检查scheduledFuture对象是否被取消,如果为false,说明当前线程在启动,否则当前线程处于关闭状态
if (!scheduledFutureMap.get(id).isCancelled()) {
return true;
}
}
return false;
}
/**
* 根据任务id 停止定时任务
* 该方法加锁,避免
*/
public void stop(Long id) {
LOGGER.info("进入关闭定时任务 :{}", id);
//首先检查当前任务实例是否存在
if (scheduledFutureMap.containsKey(id)) {
/**
* 此处添加锁,为了确保只有一个线程在执行以下逻辑,防止多人停止多次
* */
boolean lock = redisUtil.lock(定时锁, 10);
try {
if (lock) {
//获取任务实例
ScheduledFuture scheduledFuture = scheduledFutureMap.get(id);
//关闭定时任务
scheduledFuture.cancel(true);
//避免内存泄露
scheduledFutureMap.remove(id);
LOGGER.info("任务{}已成功关闭", id);
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 释放锁
redisUtil.deleteLock(定时锁);
}
}else {
LOGGER.info("当前任务{}不存在,请重试!", id);
}
}
public void init(List<RemindInfoEntity> remindInfoList){
LOGGER.info("定时任务开始初始化 ,总共:size={}个", remindInfoList.size());
//如果集合为空,则直接退出当前方法
if (CollectionUtils.isEmpty(remindInfoList)) {
return;
}
//遍历所有提醒基础信息列表
for(RemindInfoEntity remindInfo : remindInfoList) {
//将提醒信息的主键作为线程唯一标识
Long id = remindInfo.getId();
//首先检查当前定时任务是否已经启动,如果已经启动,则跳出当次循环,继续检查下一个定时任务
if (this.isStart(id)) {
continue;
}
//启动定时任务
this.doStart(remindInfo);
}
}
/**
* 启动定时任务(该方法设置为私有方法,不开放给对象直接调用)
*/
private void doStart(RemindInfoEntity remindInfo) {
/**
* 通过自动注入,生成一个被spring统一管理的实例对象taskScheduledThread
* 此处相当于创建一个定时任务,因为是实现Runable接口,还没开始创建线程
* */
TaskScheduledThread scheduledThread = new TaskScheduledThread();
/**
* 此处相当于构造定时任务的基础信息
* */
scheduledThread.setTaskId(remindInfo.getId());
scheduledThread.setTaskTyle(remindInfo.getRemindType());
scheduledThread.setTaskContent(remindInfo.getRemindContent());
LOGGER.info("正在启动任务类型:{} ,内容:{},时间表达式:{}", remindInfo.getRemindType(), remindInfo.getRemindContent(),remindInfo.getCron());
/**
* 此处使用ThreadPoolTaskScheduler的schedule方法创建一个周期性执行的任务
*
* */
ScheduledFuture<?> scheduledFuture = threadPoolTaskScheduler.schedule(scheduledThread,
triggerContext -> {
CronTrigger cronTrigger = new CronTrigger(remindInfo.getCron());
return cronTrigger.nextExecutionTime(triggerContext);
});
//将已经启动的定时任务实例放入scheduledFutureMap进行统一管理
scheduledFutureMap.put(remindInfo.getId(), scheduledFuture);
LOGGER.info("启动任务:{} 成功!",remindInfo.getId());
}
}
/**
* 定时任务启动类型
* 通过重写ApplicationRunner接口的run方法,实现项目在启动完毕时自动开启需要启动的定时任务(重启系统时会关闭所有定时任务)
* */
//@Order(value = 1)//该注解用于标识当前作用范围的执行优先级别, 默认是最低优先级,值越小优先级越高
@Component
public class TaskScheduledRunner implements ApplicationRunner {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(TaskScheduledRunner.class);
@Autowired
private RemindInfoService remindInfoService;
@Autowired
private TaskScheduledService taskScheduledService;
/**
* 系统在重启完成后,自动初始化当前系统中所有定时任务程序
*/
@Override
public void run(ApplicationArguments applicationArguments) {
LOGGER.info("系统重启中,正在重新启动定时任务程序!");
//查询状态为“启动”的提醒任务列表
Map<String,Object> param = new HashMap<>();
param.put("status", 1);
//此处没有分页结构,一旦数据比较多时,有可能会造成内存溢出
List<RemindInfoEntity> remindInfoList = remindInfoService.getList(param);
//初始化任务调度程序,重新启动所有任务调度程序
taskScheduledService.init(remindInfoList);
LOGGER.info("定时任务程序启动完成!");
}
}