有时系统中需要定时任务做别的事情,但是简单的定时任务是无法人为去控制的。
在SpringBoot中可以通过@EnableScheduling注解和@Scheduled注解实现定时任务,也可以通过SchedulingConfigurer接口来实现定时任务。但是这两种方式不能动态添加、删除、启动、停止任务。
要实现上面的需求,一般来说可以使用框架——Quartz框架。
下面要说的就是不去依赖别的定时任务框架实现需求。

本篇博客所分享知识非本人原创,参考某一日在微信看到的一篇公众号发的文章,目前找不到了。

添加执行定时任务的线程池配置类

package com.likegakki.springbootschedul.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

/**
 * date: 2021/7/24 16:14
 *
 * @author LOVEGAKKI
 * Description: 添加执行定时任务的线程池配置类
 */
@Configuration
public class SchedulingConfig {


    @Bean
    public TaskScheduler taskScheduler(){
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        //定时任务执行线程池核心数
        taskScheduler.setPoolSize(4);
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler-");
        return taskScheduler;
    }
}

添加ScheduledFuture的包装类。ScheduledFuture是ScheduledExecutorService定时任务线程池的执行结果。

package com.likegakki.springbootschedul.scheduler;

import java.util.concurrent.ScheduledFuture;

/**
 * date: 2021/7/24 16:17
 *
 * @author LOVEGAKKI
 * Description:
 */

public final class ScheduledTask {

    volatile ScheduledFuture<?> future;

    /**
     * 取消定时任务
     */
    public void cancel(){
        ScheduledFuture<?> future = this.future;
        if (future != null){
            future.cancel(true);
        }
    }
}

添加Runnable接口实现类,被定时任务线程池调用,用来执行指定bean里面的方法。

package com.likegakki.springbootschedul.scheduler;

import com.likegakki.springbootschedul.util.SpringContextUtils;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

/**
 * date: 2021/7/24 16:23
 *
 * @author LOVEGAKKI
 * Description: 添加Runnable接口实现类,被定时任务线程池调用,用来执行指定bean里面的方法。
 */
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class SchedulingRunnable implements Runnable{


    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);

    private String beanName;

    private String methodName;

    private String params;

    public SchedulingRunnable(String beanName,String methodName){
        this(beanName,methodName,null);
    }

    @Override
    public void run() {
        logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params);
        long startTime = System.currentTimeMillis();

        try{
            Object target = SpringContextUtils.getBean(beanName);

            Method method= null;

            if (!StringUtils.isEmpty(params)){
                method = target.getClass().getDeclaredMethod(methodName,String.class);
            }else {
                method = target.getClass().getDeclaredMethod(methodName);
            }

            ReflectionUtils.makeAccessible(method);

            if (!StringUtils.isEmpty(params)){
                method.invoke(target,params);
            }else {
                method.invoke(target);
            }
        }catch (Exception e){
            logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), e);
        }
        long times = System.currentTimeMillis() - startTime;
        logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
    }
}

@NoArgsConstructor,@AllArgsConstructor,@EqualsAndHashCode
这三个注解是来自Lombok,分别是:无参构造,全参构造,重写Equals和HashCode方法。
这里使用的SpringContextUtils代码如下:

package com.likegakki.springbootschedul.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    public static <T> T getBean(String name, Class<T> requiredType) {
        return applicationContext.getBean(name, requiredType);
    }

    public static boolean containsBean(String name) {
        return applicationContext.containsBean(name);
    }

    public static boolean isSingleton(String name) {
        return applicationContext.isSingleton(name);
    }

    public static Class<? extends Object> getType(String name) {
        return applicationContext.getType(name);
    }
}

添加定时任务注册类,用来增加、删除定时任务。

package com.likegakki.springbootschedul.scheduler;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * date: 2021/7/24 18:02
 *
 * @author LOVEGAKKI
 * Description: 添加定时任务注册类,用来增加、删除定时任务。
 */
@Component
public class CronTaskRegistrar implements DisposableBean {

    private final Map<Runnable,ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);

    @Autowired
    private TaskScheduler taskScheduler;

    public TaskScheduler getTaskScheduler(){
        return this.taskScheduler;
    }

    public void addCronTask(Runnable task,String cronExpression){
        addCronTask(new CronTask(task,cronExpression));
    }

    public void addCronTask(CronTask cronTask) {
        if (cronTask != null){
            Runnable task = cronTask.getRunnable();
            if (this.scheduledTasks.containsKey(task)){

                removeCronTask(task);
            }

            this.scheduledTasks.put(task,scheduledCronTask(cronTask));
        }
    }

    public void removeCronTask(Runnable task){
        ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
        if (scheduledTask != null){
            scheduledTask.cancel();
        }
    }

    public ScheduledTask scheduledCronTask(CronTask cronTask){
        ScheduledTask scheduledTask = new ScheduledTask();
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(),cronTask.getTrigger());

        return scheduledTask;
    }

    @Override
    public void destroy() throws Exception {

        for (ScheduledTask task : this.scheduledTasks.values()){
            task.cancel();
        }

        this.scheduledTasks.clear();
    }
}

编写定时任务代码

package com.likegakki.springbootschedul.scheduler;

import org.springframework.stereotype.Component;

/**
 * date: 2021/7/24 20:11
 *
 * @author LOVEGAKKI
 * Description:
 */
@Component("demoTask")
public class DemoTask {

    public void taskWithParams(String param){
        System.out.println("执行有参示例任务:" + param);
    }

    public void taskNoParams(){
        System.out.println("执行无参示例任务");
    }
}

创建定时任务实体类对象

package com.likegakki.springbootschedul.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * date: 2021/7/24 20:17
 *
 * @author LOVEGAKKI
 * Description:
 */
@ApiModel("定时任务实体类")
@Data
@TableName("system_job")
public class SystemJobEntity implements Serializable {

    /**
     * 任务ID
     */
    @TableId(type = IdType.AUTO)
    private Integer jobId;
    /**
     * bean名称
     */
    private String beanName;
    /**
     * 方法名称
     */
    private String methodName;
    /**
     * 方法参数
     */
    private String methodParams;
    /**
     * cron表达式
     */
    private String cronExpression;
    /**
     * 状态(1正常 0暂停)
     */
    private Integer jobStatus;
    /**
     * 备注
     */
    private String remark;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;
}

定时任务状态枚举类

package com.likegakki.springbootschedul.enums;

public enum SystemJobStatus {

    /**
     * 暂停
     */
    PAUSE,

    /**
     * 正常
     */
    NORMAL
}

对定时任务的增删改查(包含增改启停)

package com.likegakki.springbootschedul.controller;

import com.likegakki.springbootschedul.entity.SystemJobEntity;
import com.likegakki.springbootschedul.enums.SystemJobStatus;
import com.likegakki.springbootschedul.scheduler.CronTaskRegistrar;
import com.likegakki.springbootschedul.scheduler.SchedulingRunnable;
import com.likegakki.springbootschedul.service.SystemJobService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * date: 2021/7/24 20:23
 *
 * @author LOVEGAKKI
 * Description:
 */
@Api(value = "SystemJobController")
@RestController
@RequestMapping("/system/job")
public class SystemJobController {


    @Autowired
    private SystemJobService systemJobService;

    @Autowired
    private CronTaskRegistrar cronTaskRegistrar;

    @ApiOperation("新增定时任务")
    @PostMapping
    public String add(@RequestBody SystemJobEntity systemJobEntity){

        boolean success = systemJobService.save(systemJobEntity);
        if (success){
        	//判断当前创建的任务状态,如果是启动,添加
            if (systemJobEntity.getJobStatus().equals(SystemJobStatus.NORMAL.ordinal())){
                SchedulingRunnable task = new SchedulingRunnable(systemJobEntity.getBeanName(), systemJobEntity.getMethodName(), systemJobEntity.getMethodParams());
                cronTaskRegistrar.addCronTask(task,systemJobEntity.getCronExpression());
            }
            return "ok";
        }
        return "error";
    }

    @ApiOperation("删除定时任务")
    @DeleteMapping("/{jobId}")
    public String delete(@PathVariable String jobId){
        SystemJobEntity systemJobEntity = systemJobService.getById(jobId);
        boolean success = systemJobService.removeById(jobId);
        if (success){
            if (systemJobEntity.getJobStatus().equals(SystemJobStatus.NORMAL.ordinal())){
                SchedulingRunnable task = new SchedulingRunnable(systemJobEntity.getBeanName(), systemJobEntity.getMethodName(), systemJobEntity.getMethodParams());
                cronTaskRegistrar.removeCronTask(task);
            }
            return "ok";
        }
        return "error";
    }

    @ApiOperation("修改定时任务")
    @PutMapping
    public String edit(@RequestBody SystemJobEntity systemJobEntity){
        SystemJobEntity oldSystemJobEntity = systemJobService.getById(systemJobEntity.getJobId());
        boolean success = systemJobService.updateById(systemJobEntity);

        if (success){
            //先移除再添加
            if (oldSystemJobEntity.getJobStatus().equals(SystemJobStatus.NORMAL.ordinal())){
                SchedulingRunnable task = new SchedulingRunnable(oldSystemJobEntity.getBeanName(), oldSystemJobEntity.getMethodName(), oldSystemJobEntity.getMethodParams());
                cronTaskRegistrar.removeCronTask(task);
            }

            if (systemJobEntity.getJobStatus().equals(SystemJobStatus.NORMAL.ordinal())){
                SchedulingRunnable task = new SchedulingRunnable(systemJobEntity.getBeanName(), systemJobEntity.getMethodName(), systemJobEntity.getMethodParams());
                cronTaskRegistrar.addCronTask(task,systemJobEntity.getCronExpression());
            }
            return "ok";
        }
        return "error";
    }

    @ApiOperation("获取所有定时任务")
    @GetMapping
    public List<SystemJobEntity> list(){
        return systemJobService.list();
    }
}

定时任务的代码肯定是提前写好的,做不到动态增加定时任务业务代码。

代码地址:https://gitee.com/likegakki/springboot-schedul