SpringBoot实现动态定时任务

SpringBoot实现动态定时任务

1. 写在前面

对于动态定时任务的实现,现在网上有很多开源的第三方框架,比如比较有有名的 xxl-job,还有很多比较好用的,因为我们没有那么复杂的功能,所有这里我就直接通过springboot的定时器为基础写了一个简单的实现。

2. 代码实现

1.首先是config配置,实例化一个调度线程池。

@Configuration
public class ScheduleConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        //线程池大小
        threadPoolTaskScheduler.setPoolSize(10);
        //
        threadPoolTaskScheduler.setRemoveOnCancelPolicy(true);
        //线程名称
        threadPoolTaskScheduler.setThreadNamePrefix("demo-task-");
        // 等待时长
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);         
        return threadPoolTaskScheduler;
    }
}

2.ScheduledTask.java,这个类应该就是将任务对象实例化成线程安全对象,外加一个取消定时任务的方法

public class ScheduledTask {

    volatile ScheduledFuture<?> future;

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

3.CronTaskRegistrar.java,这个定时任务的操作类了,包括添加定时任务,删除定时任务,以及在项目停止时去终止所有的定时任务。

@Component
public class CronTaskRegistrar implements DisposableBean {
    private final Map<Runnable, ScheduledTask> scheduledTaskMap = new ConcurrentHashMap<>(16);
    @Autowired
    private TaskScheduler taskScheduler;
    /**
     * 新增定时任务
     * @param task
     * @param cron
     */
    public void addTask(Runnable task, String cron) {
        addTask(new CronTask(task, cron));
    }
    public void addTask(CronTask cronTask) {
        if(cronTask != null) {
            Runnable task = cronTask.getRunnable();
            if(this.scheduledTaskMap.containsKey(task)) {
                removeCronTask(task);
            }
            this.scheduledTaskMap.put(task, scheduledCronTask(cronTask));
        }
    }
    /**
     * 移除定时任务
     * @param task
     */
    public void removeCronTask(Runnable task) {
        System.out.println("清除定时任务"+task);
        ScheduledTask scheduledTask = this.scheduledTaskMap.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.scheduledTaskMap.values()) {
            task.cancel();
        }
        this.scheduledTaskMap.clear();
    }
}

这里用一个线程安全的Map(ConcurrentHashMap)去存放所有正在执行的定时任务,来方便动态操作。有添加和删除任务的方法,
注意:这里用Runnable作为Map的key,所以我们要注意需要重写Runnable实现类的hashCode和equals方法,具体如何判断是否是同一任务可以按自己的需求写。

3.ScheduleRunnalbe.java,这个类就是实现任务的通用线程类,将业务方法当做参数传进来去创建一个定时器的线程任务。


/**
 * 这个类的作用是将所有的业务任务方法都通过这个类包装一下,通过spring获取bean对象和java反射获取执行方法去执行实际业务,也可以想xxl一样定义一个注解,然后去扫描这个注解获取业务方法也行
 * 用重写equals和hashcode方法控制任务的唯一性,同时做通用日志打印,现在用bean、方法名、参数来检验唯一性,可根据业务是否添加cron条件
 */
@Slf4j
public class ScheduleRunnalbe implements Runnable {

    private String cron;
    private String beanName;
    private String methodName;
    private Object[] params;

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

    @Override
    public void run() {
        log.info("定时任务开始执行:beanName{},methodName{},params{}, cron{}", beanName, methodName, params, cron);
        try {
            Object target = SpringContextUtil.getBean(beanName);
            Method method = null;
            if (params != null && params.length > 0) {
                Class<?>[] paramCls = new Class[params.length];
                for (int i = 0; i < params.length; i++) {
                    paramCls[i] = params[i].getClass();
                }
                method = target.getClass().getDeclaredMethod(methodName, paramCls);
            } else {
                method = target.getClass().getDeclaredMethod(methodName);
            }

            ReflectionUtils.makeAccessible(method);
            if(params != null && params.length > 0) {
                method.invoke(target, params);
            } else {
                method.invoke(target);
            }
        } catch (Exception e) {
            log.error("定时任务报错e={}", e);
        }
        log.info("定时任务结束:beanName{},methodName{},params{}, cron{}", beanName, methodName, params, cron);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ScheduleRunnalbe that = (ScheduleRunnalbe) 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) &&
                Arrays.equals(params, that.params);
    }

    @Override
    public int hashCode() {
        if (params == null) {
            return Objects.hash(beanName, methodName);
        }

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

这里重写了hashCode()和equals()方法,我这里用beanName和MethodName以及params作为唯一标准写的,也可以将定时任务的表达式加进来一起组合成唯一标准。
这里是通过spring获取bean对象来获取类,然后通过传入的方法名用反射获取方法进行调用的,所以得写到spring里,也可以改造成别的方式,只要保证任务的唯一性,像xxl就是通过自定义了一个注解,然后扫描这个注解去获取方法执行的任务。

4.SpringContextUtil.java,这个类主要用来通过spring上下文来获取bean对象用的。

package com.es.demo.task;

import com.sun.istack.internal.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.Objects;


/**
 * 类名: SpringContextUtil
 * 描述:获取bean的工具类,可用于在线程里面获取bean
 * 时间: 2023年05月16日18:18:49
 * @author sunlingtong
 */
@Component("SpringContextUtil")
public class SpringContextUtil implements ApplicationContextAware {


    public static final String LOCAL_PROFILE = "local";

    public static final String DEV_PROFILE = "dev";

    public static final String TEST_PROFILE = "test";

    public static final String PRO_PROFILE = "pro";

    private static ApplicationContext applicationContext = null;
    private SpringContextUtil() {
        super();
    }
    public static synchronized ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    // 下面的这个方法上加了@Override注解,原因是继承ApplicationContextAware接口是必须实现的方法
    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext)
            throws BeansException {
        synchronized (this) {
            if (Objects.isNull(SpringContextUtil.applicationContext)) {
                SpringContextUtil.applicationContext = applicationContext;
            }
        }
    }
    /**
     * @param beanName bean名称
     * @Description: 获取spring容器中的bean, 通过bean名称获取
     * @return: Object 返回Object,需要做强制类型转换
     * @time: 2023-02-28 10:45:07
     */
    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }
    /**
     * @param beanClass bean 类型
     * @Description: 获取spring容器中的bean, 通过bean类型获取
     * @return: T 返回指定类型的bean实例
     * @time: 2023-02-28 10:46:31
     */
    public static <T> T getBean(Class<T> beanClass) {
        return applicationContext.getBean(beanClass);
    }
    /**
     * @param beanName  bean 名称
     * @param beanClass bean 类型
     * @Description: 获取spring容器中的bean, 通过bean名称和bean类型精确获取
     * @return: T 返回指定类型的bean实例
     * @time: 2023-02-28 10:47:45
     */
    public static <T> T getBean(String beanName, Class<T> beanClass) {
        return applicationContext.getBean(beanName, beanClass);
    }
    /**
     * @param beanClass bean 类型
     * @Description: 根据 bean 的 class 来查找所有的对象(包括子类)
     * @return: T 返回指定类型的bean实例
     * @time: 2023-02-28 10:47:45
     */
    public static <T> Map<String, T> getBeansByClass(Class<T> beanClass) {
        return applicationContext.getBeansOfType(beanClass);
    }
    /**
     * 是否包含bean
     *
     * @param beanName bean 名称
     */
    public static boolean containsBean(String beanName) {
        return applicationContext.containsBean(beanName);
    }
    /**
     * 获取HttpServletRequest
     */
    public static HttpServletRequest getHttpServletRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getRequest();
    }
    /**
     * 获取HttpSession
     */
    public static HttpSession getSession() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attributes.getRequest().getSession();
    }
    public static String getDomain() {
        HttpServletRequest request = getHttpServletRequest();
        StringBuffer url = request.getRequestURL();
        return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
    }
    /**
     * 获取请求头
     */
    public static String getOrigin() {
        HttpServletRequest request = getHttpServletRequest();
        return request.getHeader("Origin");
    }
    /**
     * 获取完整的请求URL
     */
    public static String getRequestUrl() {
        return getRequestUrl(getHttpServletRequest());
    }
    /**
     * 获取完整的请求URL
     */
    public static String getRequestUrl(HttpServletRequest request) {
        // 当前请求路径
        String currentUrl = request.getRequestURL().toString();
        // 请求参数
        String queryString = request.getQueryString();
        if (!ObjectUtils.isEmpty(queryString)) {
            currentUrl = currentUrl + "?" + queryString;
        }
        String result = "";
        try {
            result = URLDecoder.decode(currentUrl, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // ignore
        }
        return result;
    }
    /**
     * 获取spring.profiles.active
     */
    public static String getProfile() {
        return getApplicationContext().getEnvironment().getActiveProfiles()[0];
    }
    /**
     * 判断是否为联调环境
     */
    public static boolean profileIsDev() {
        return DEV_PROFILE.equals(getProfile());
    }
    /**
     * 判断是否为本地开发环境
     */
    public static boolean profileIsLocal() {
        return LOCAL_PROFILE.equals(getProfile());
    }
    /**
     * 判断是否为测试环境
     */
    public static boolean profileIsTest() {
        return TEST_PROFILE.equals(getProfile());
    }
    /**
     * 判断是否为生产环境
     */
    public static boolean profileIsPro() {
        return PRO_PROFILE.equals(getProfile());
    }
    /**
     * 是否是 Linux 环境
     *
     * @return
     */
    public static boolean isLinux() {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
    public static boolean isMac() {
        return System.getProperty("os.name").toLowerCase().contains("mac");
    }
    /**
     * 是否是 Windows 环境
     *
     * @return
     */
    public static boolean isWindows() {
        return System.getProperty("os.name").toLowerCase().contains("windows");
    }
}

5.BussinessTask.java,写一个任务类测试使用

@Component("bussinessTask")
public class BussinessTask {

    public void test() {
        System.out.println("这是定时任务test");
    }

    public void test(String aaa) {
        System.out.println("这是定时任务test(aaa),aaa="+aaa);
    }
}

具体的实现到这里就完事了,下面是写个test测试一下

@SpringBootTest
class EsDemoApplicationTests {

    @Autowired
    private CronTaskRegistrar cronTaskRegistrar;
    @Test
    void contextLoads() {
        ScheduleRunnalbe scheduledTask = new ScheduleRunnalbe("0/3 * * * * ?","bussinessTask","test");
        cronTaskRegistrar.addTask(scheduledTask, "0/3 * * * * ?");

        ScheduleRunnalbe scheduledTask2 = new ScheduleRunnalbe("0/5 * * * * ?","bussinessTask","test","123");
        cronTaskRegistrar.addTask(scheduledTask2,"0/5 * * * * ?");

        try {
            Thread.sleep(60000L);
        } catch (Exception e) {

        }
        //动态修改定时任务执行时间
        ScheduleRunnalbe scheduledTask3 = new ScheduleRunnalbe("0/8 * * * * ?","bussinessTask","test","123");
        cronTaskRegistrar.addTask(scheduledTask3,"0/8 * * * * ?");

        try {
            Thread.sleep(10000000L);
        } catch (Exception e) {

        }
    }

}

结果就不截图了。

总结

上面就简单实现了一个动态调度的方式,具体在项目中如何使用呢,可以将所有的配置写到数据库中,然后写一个方法,在项目启动完成后去读取数据库中的信息,通过@PostConstruct这个注解去实现

@PostConstruct
public void start() {
    String osName = System.getProperty("os.name");
    System.out.println(osName);
    if (SpringContextUtil.isWindows()) {
        return;
    }
    //读取数据库配置信息,循环实例化任务线对象加入到注册器(CronTaskRegistrar)中就行了
}

因为这比较简单,所以在写的时候也没遇到啥问题和总结,就这样吧

参考: https://www.jianshu.com/p/98e87ec9b0f1

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot通过@EnableScheduling注解来开启定时任务的功能。下面是实现动态定时任务的步骤: 1. 首先,在你的Spring Boot应用的主类上添加@EnableScheduling注解,启用定时任务的支持。例如: ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } } ``` 2. 接下来,在需要执行定时任务的方法上添加@Scheduled注解,并设置定时任务的执行规则。例如,我们可以使用cron表达式来定义定时任务的执行时间。以下是一个示例: ```java import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class MyScheduledTask { @Scheduled(cron = "0 0/5 * * * *") // 每5分钟执行一次 public void doSomething() { // 定时任务要执行的操作 } } ``` 3. 现在,当应用启动后,定时任务会按照定义的规则自动执行。 如果你想在运行时动态地修改定时任务的执行规则,可以借助Spring的ScheduledTaskRegistrar类。可以在应用程序中注入ScheduledTaskRegistrar对象,然后使用其方法来注册、取消和修改定时任务。这样你就可以实现动态定时任务调度了。 希望这个回答对你有帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值