基于SpirngBoot实现动态定时任务

  |   0 评论   |   0 浏览

基于SpirngBoot实现动态定时任务

1.执行流程

一图胜千言

image-20220309170946199

2.准备工作

1.建Springboot工程

2.引入依赖

这里只需要引入lombok和web依赖即可

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

3.代码实现

主启动

@SpringBootApplication
public class TimetaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(TimetaskApplication.class, args);
    }

}

1.SpringContextUtils

package com.llp.timetask.task;

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


/**
 * @ClassName SpringContextUtils
 * @Description: spring获取bean工具类
 * @Author llp
 * @Date Created in 2020/12/17
 **/
@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

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

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

2.任务线程

package com.llp.timetask.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.util.Objects;

/**
 * @ClassName SendMsgTask
 * @Description: 定时任务运行类
 * @Author llp
 * @Date Created in 2020/12/17
 **/
@Slf4j
public class SchedulingRunnable implements Runnable {


    private String beanName;

    private String methodName;

    private Object[] params;

    public SchedulingRunnable(String beanName, String methodName) {
        this(beanName, methodName, new Object[0]);
    }

    public SchedulingRunnable(String beanName, String methodName, Object ...params ) {
        this.beanName = beanName;
        this.methodName = methodName;
        this.params = params;
    }

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

        try {
            //获取定时任务Job类
            Object target = SpringContextUtils.getBean(beanName);

            Method method = null;
            //判断定时任务是否携带参数
            if (null != params && params.length > 0) {
                //根据参数长度定义Class数组的长度
                Class<?>[] paramCls = new Class[params.length];
                for (int i = 0; i < params.length; i++) {
                    //获取每个参数的运行时类型,并赋值给Class数组的每个元素
                    paramCls[i] = params[i].getClass();
                }
                //target.getClass()获取Class对象,在通过Class对象获取带参Method对象
                method = target.getClass().getDeclaredMethod(methodName, paramCls);
            } else {
                //获取不带参的Method对象
                method = target.getClass().getDeclaredMethod(methodName);
            }
            /**
             * 使给定的方法可访问,必要时显式设置它可访问。
             * {@code setAccessible(true)}
             * 方法仅在实际需要时调用,以避免与 JVM SecurityManager(如果处于活动状态)发生不必要的冲突。
             * 可以简单理解,方法如果是非public修饰的,则进行爆破(关闭访问检查)
             * 	method.setAccessible(true);
             */
            // method.setAccessible(true);
            ReflectionUtils.makeAccessible(method);
            if (null != params && params.length > 0) {
                //执行有参方法
                method.invoke(target, params);
            } else {
                //执行无参方法
                method.invoke(target);
            }
        } catch (Exception ex) {
            log.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex);
        }
        //计算定时任务执行耗时时间(单位:毫秒)
        long times = System.currentTimeMillis() - startTime;
        log.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
    }

    /**
     * 重新equals方法
     * 1.比较两个对象的引用地址是否相同
     * 2.考虑通过反射获取对象的方式,Class类型应该为同一个对象
     * 3.如果创建的是不同的对象则根据构造参数进行比较 beanName 和 methodName是必填的,因此对外不提供无参构造
     * (1)如果params为空,则根据beanName,methodName来判断对象是否属于同一个对象
     * (2)如果params不为空,则根据beanName,methodName,params来判断对象是否属于同一个对象
     * @param o
     * @return
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        SchedulingRunnable that = (SchedulingRunnable) o;
        if (params == null) {
            return beanName.equals(that.beanName) &&
                    methodName.equals(that.methodName) &&
                    that.params == null;
        }

        return beanName.equals(that.beanName) &&
                methodName.equals(that.methodName) &&
                params.equals(that.params);
    }

    /**
     * hash值的比较也是同理
     * (1)如果params为空,则根据beanName,methodName来判断对象是否属于同一个对象
     * (2)如果params不为空,则根据beanName,methodName,params来判断对象是否属于同一个对象
     * @return
     */
    @Override
    public int hashCode() {
        if (params == null) {
            return Objects.hash(beanName, methodName);
        }

        return Objects.hash(beanName, methodName, params);
    }
}

3.CronTaskRegistrar用于管理定时任务

package com.llp.timetask.task;

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

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName CronTaskRegistrar
 * @Description: 添加定时任务注册类,用来增加、删除定时任务。
 * @Author llp
 * @Date Created in 2020/12/17
 **/
@Component
public class CronTaskRegistrar implements DisposableBean {
    /**
     * 考虑后面可能会有多个区县同时添加定时任务的可能性
     * 创建ConcurrentHashMap集合用于存放定时任务
     */
    private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);


    @Resource
    private TaskScheduler taskScheduler;

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

    /**
     * 新增定时任务
     * @param task
     * @param cronExpression
     */
    public void addCronTask(Runnable task, String cronExpression) {
        addCronTask(new CronTask(task, cronExpression));
    }

    public void addCronTask(CronTask cronTask) {
        if (cronTask != null) {
            //获取任务线程类
            Runnable task = cronTask.getRunnable();
            //判断map集合中是否存在对应的key,如果存在则删除之前按的key,以保证最新的定时任务
            if (this.scheduledTasks.containsKey(task)) {
                removeCronTask(task);
            }
            this.scheduledTasks.put(task, scheduleCronTask(cronTask));
        }
    }

    /**
     * 移除定时任务
     * @param task
     */
    public void removeCronTask(Runnable task) {
        ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
        if (scheduledTask != null) {
            scheduledTask.cancel();
        }
    }

    /**
     *
     * @param cronTask
     * @return
     */
    public ScheduledTask scheduleCronTask(CronTask cronTask) {
        //定时任务控制类
        ScheduledTask scheduledTask = new ScheduledTask();
        /**
         * @param1:任务线程类
         * @param2:触发器
         */
        //ScheduledFuture<?> schedule调度给定的 {@link Runnable},只要触发器指示下一个执行时间就调用它。
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
        return scheduledTask;
    }

    /**
     * 在销毁 bean 时由包含 {@code BeanFactory} 调用。
     * @throws 出现关闭错误时的异常。异常将被记录但不会重新抛出以允许其他 bean 也释放它们的资源
     */
    @Override
    public void destroy() {
        for (ScheduledTask task : this.scheduledTasks.values()) {
            task.cancel();
        }
        this.scheduledTasks.clear();
    }
}

4.定时任务控制类

package com.llp.timetask.task;

import java.util.concurrent.ScheduledFuture;

/**
 * @ClassName ScheduledTask
 * @Description: 定时任务控制类
 * @Author llp
 * @Date Created in 2020/12/17
 **/
public final class ScheduledTask {

    public volatile ScheduledFuture<?> future;
    /**
     * 取消定时任务
     */
    public void cancel() {
        ScheduledFuture<?> future = this.future;
        if (future != null) {
            //true会尝试去中断正在运行的定时任务
            future.cancel(true);
        }
    }
}

5.线程池配置

package net.cqnews.fifth.task.sendtiming;

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

/**
 * @ClassName SchedulingConfig
 * @Description: 定时任务配置类
 * @Author llp
 * @Date Created in 2020/12/17
 **/
@Configuration
public class SchedulingConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        // 定时任务执行线程池核心线程数
        taskScheduler.setPoolSize(4);
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
        return taskScheduler;
    }
}

3.测试

1.Controller

@RestController
public class MyTestController {

    @Resource
    private MessageSendService messageSendService;

    @GetMapping("/addTask")
    public void addTask(@RequestParam(name = "content",required = true) String content){
        messageSendService.sendMsg(content);
    }
}

2.service

@Service
public class MessageSendService {

    @Resource
    private CronTaskRegistrar cronTaskRegistrar;

    public void sendMsg(String content) {
        //模拟发送时间,为当前时间的1分钟之后执行
        LocalDateTime sendTime = LocalDateTime.now();
        int second = sendTime.getSecond();
        int minute = sendTime.getMinute()+1;
        int hour = sendTime.getHour();
        int dayOfMonth = sendTime.getDayOfMonth();
        int monthValue = sendTime.getMonthValue();

        //1.创建SchedulingRunnable线程类
        SchedulingRunnable task = new SchedulingRunnable("SendMsgTask","sendTiming",content);
        //2.添加任务
        cronTaskRegistrar.addCronTask(task, second + " " + minute + " " + hour + " " + dayOfMonth + " " + monthValue + " ? ");
    }
}

3.Job类

package com.llp.timetask.task;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @ClassName SendMsgTask
 * @Description: 定时发送任务
 * @Author llp
 * @Date Created in 2020/12/17
 **/
@Slf4j
@Component("SendMsgTask")
@RequiredArgsConstructor
public class SendMsgTask {

    /**
     * 定时发送
     */
    public void sendTiming(String content) {
        //TODO 发送相关的业务
        System.out.println("定时任务执行,发送内容: "+content);
    }

}

测试结果

定时任务开始执行 - bean:SendMsgTask,方法:sendTiming,参数:[孙悟空]
定时任务执行,发送内容: 孙悟空
2022-03-09 16:31:20.016  INFO 17692 --- [lerThreadPool-1] c.llp.timetask.task.SchedulingRunnable   : 定时任务执行结束 - bean:SendMsgTask,方法:sendTiming,参数:[孙悟空],耗时:1 毫秒

标题:基于SpirngBoot实现动态定时任务
作者:llp
地址:https://llinp.cn/articles/2022/03/09/1646816180824.html