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)中就行了
}
因为这比较简单,所以在写的时候也没遇到啥问题和总结,就这样吧
版权归原作者 站在墙头上 所有, 如有侵权,请联系我们删除。