在开发过程中经常遇到需要定时处理的一些任务, 例如每天的消费提醒、打卡等等. 简单的操作时直接在代码中写一个周期循环的方法, 例如:工作日每天上午10点整提醒用户打卡,
但是这样做有个缺陷, 我们并没有办法去控制该任务调度的开启和关闭, 以下代码提供一种可控制的定时任务调度.

1. 导入相关依赖

<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>

2.定义任务调度的提醒信息实体类

@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;
}

3.定义controller层接口

3.1 启动定时任务的接口

@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 "启动异常,请联系管理员进行处理";
    }

3.2 关闭定时任务的接口

@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 "停止失败,请联系管理员";
    }

4. 构建service层

/**
 *  提醒信息业务逻辑层
 *
 * 注:mybatis-plus可以 extends ServiceImpl<RemindInfoMapper, RemindInfo>,然后使用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;
    }
}

5. 编写定时任务实现类

5.1 编写上线文工具类

/**
 *  用于获取上文实例对象
 * 该工具在项目中是用于在多线程中获取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);
    }


}

5.2 定义一个线程类,用于实现定时任务具体执行的操作逻辑

/**
 *  任务定时程序线程类
 * 用于实现线程的执行逻辑
 * */
@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)));
    }
}

5.3 实现动态创建定时任务

  • 创建定时任务类(TaskScheduledService )
/**
 * 定时任务业务逻辑层
 * */
@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;
  • 根据任务id 启动定时任务
/**
     * @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 判断定时任务是否启动
/**
     * 根据任务id 判断定时任务是否启动
     */
    public Boolean isStart(Long id) {
        //首先检查scheduledFutureMap是否存在该任务,如果不存在,则确定当前任务并没有启动
        if (scheduledFutureMap.containsKey(id)) {
            //当该任务存在时,需要检查scheduledFuture对象是否被取消,如果为false,说明当前线程在启动,否则当前线程处于关闭状态
            if (!scheduledFutureMap.get(id).isCancelled()) {
                return true;
            }
        }
        return false;
    }
  • 根据任务id 停止定时任务
/**
     * 根据任务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());
    }
}

5.4 创建定时任务启动类。此处主要是重写ApplicationRunner接口的run方法,就可以做到在项目启动时自动创建与启动定时任务

/**
 * 定时任务启动类型
 * 通过重写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("定时任务程序启动完成!");
    }
}

运行截图

java 调度sch_定时任务


java 调度sch_java_02


java 调度sch_定时任务_03

java 调度sch_java 调度sch_04