Spring定时任务玩出花
1. 项目概览
我们首先来大概看下这个项目:
这里和定时任务相关的配置主要在 config 包里边,其他的都是业务类代码,换句话说其他的都是常规的 CURD,所以我这里主要和小伙伴们介绍 config 中的代码。 2. 整体思路
我先来说说这个项目的整体思路,这样方便大家理解下面的内容。
在这个项目中,每一个定时任务都由一个线程去处理,负责处理每一个定时任务的线程类是 SchedulingRunnable,所有的线程都跑在一个线程池中,这个线程池是 ThreadPoolTaskScheduler,这是一个专为定时任务设计的线程池(支持 Cron 表达式),它的底层其实就是大家所熟知的 ScheduledThreadPoolExecutor。当有一个新的定时任务需要执行时,创建一个 SchedulingRunnable 线程,然后连同 Cron 表达式一起扔到 ThreadPoolTaskScheduler 池子里去执行就行了。 3. 配置分析
几个配置类我们逐一来分析。 3.1 SpringContextUtils
首先我们提供了一个 SpringContextUtils 工具类,这个工具类实现了 ApplicationContextAware 接口,通过这个工具类,我们可以从 Spring 容器中查询一个 Bean 或者判断 Spring 容器中是否存在某一个 Bean,工具类的代码如下(我主要列出来了有哪些方法,具体实现大家可以参考:https://github.com/lenve/scheduling): @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) { } public static T getBean(Class requiredType) { } public static T getBean(String name, Class requiredType) { } public static boolean containsBean(String name) { } public static boolean isSingleton(String name) { } public static Class<? extends Object> getType(String name) { } } 3.2 SchedulingRunnable
将来每一个定时任务执行的时候,我们都开启一个新的线程去执行这个定时任务,SchedulingRunnable 就是关于这个线程的配置,我们来看下: public class SchedulingRunnable implements Runnable { private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class); private String beanName; private String methodName; private String params; private Object targetBean; private Method method; public SchedulingRunnable(String beanName, String methodName) { this(beanName, methodName, null); } public SchedulingRunnable(String beanName, String methodName, String params) { this.beanName = beanName; this.methodName = methodName; this.params = params; init(); } private void init() { try { targetBean = SpringContextUtils.getBean(beanName); if (StringUtils.hasText(params)) { method = targetBean.getClass().getDeclaredMethod(methodName, String.class); } else { method = targetBean.getClass().getDeclaredMethod(methodName); } ReflectionUtils.makeAccessible(method); } catch (NoSuchMethodException e) { e.printStackTrace(); } } @Override public void run() { logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params); long startTime = System.currentTimeMillis(); try { if (StringUtils.hasText(params)) { method.invoke(targetBean, params); } else { method.invoke(targetBean); } } catch (Exception ex) { logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex); } long times = System.currentTimeMillis() - startTime; logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times); } @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); } @Override public int hashCode() { if (params == null) { return Objects.hash(beanName, methodName); } return Objects.hash(beanName, methodName, params); } }
SchedulingRunnable 实现了 Runnable 接口,这里的实现逻辑也比较简单,我们一起来看下: 首先声明了 beanName、methodName 以及 params 分别作为定时任务执行的 Bean 的 bean 名称、方法名称以及方法参数。不知道小伙伴们是否记得我们上篇文章中介绍的该系统的用法,在添加一个定时任务时,我们需要传入相应的 beanName、methodName 以及 params 参数,传入后就来到这里了。另外还有 targetBean 和 method 分别表示 beanName 对应的对象以及 methodName 对应的对象,其中 targetBean 通过 beanName 从 Spring 容器中查找,method 则通过 methodName 从 targetBean 中查找。 在 run 方法中,通过反射去调用 method 方法,这也是定时任务执行时候的具体逻辑。 另外,这里重写了 equals 和 hashCode 方法,这两个方法主要是比较了 beanName、methodName 以及 params 三个属性,换言之,如果这三个属性相同,则认为这是同一个对象(这三个属性相同表示这是同一个定时任务)。 3.3 SchedulingConfig@Configuration public class SchedulingConfig { @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(4); taskScheduler.setRemoveOnCancelPolicy(true); taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-"); return taskScheduler; } }
这里主要是配置一下 ThreadPoolTaskScheduler,这个可以很方便的对重复执行的任务进行调度管理,相比于通过 Java 自带的周期性任务线程池ScheduleThreadPoolExecutor,ThreadPoolTaskScheduler 对象支持根据 Cron 表达式创建周期性任务。
既然是线程池,必然就有线程数量等问题,它的核心线程池大小就是我们配置的 poolSize 属性,最大线程池大小是 Integer.MAX_VALUE ,keepAliveTime 为 0 ,这里用到的队列是 DelayedWorkQueue ,这个队列有一个属性 private final DelayQueue dq = new DelayQueue(); 对这个队列的操作实际是是对这个 DelayQueue 的操作,这个队列大小是 Integer.MAX_VALUE,所以线程数量肯定是够用了。
其他配置就没啥好说的。 3.4 ScheduledTask
ScheduledTask 是 ScheduledFuture 的包装类,这个包装类中主要多了一个 future 属性,这个 future 属性表示 TaskScheduler 定时任务线程池的执行结果: public final class ScheduledTask { volatile ScheduledFuture<?> future; public void cancel() { ScheduledFuture<?> future = this.future; if (future != null) { future.cancel(true); } } } 3.5 CronTaskRegistrar
核心的方法都在这个里边。 @Component public class CronTaskRegistrar implements DisposableBean { private final Map scheduledTasks = new ConcurrentHashMap<>(16); @Autowired private TaskScheduler taskScheduler; public TaskScheduler getScheduler() { 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, scheduleCronTask(cronTask)); } } public void removeCronTask(Runnable task) { ScheduledTask scheduledTask = this.scheduledTasks.remove(task); if (scheduledTask != null) scheduledTask.cancel(); } public ScheduledTask scheduleCronTask(CronTask cronTask) { ScheduledTask scheduledTask = new ScheduledTask(); scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger()); return scheduledTask; } @Override public void destroy() { for (ScheduledTask task : this.scheduledTasks.values()) { task.cancel(); } this.scheduledTasks.clear(); } }
稍微说下这个类: 首先这个类实现了 DisposableBean 接口,实现这个接口就重写了 destroy 方法,以便在 Bean 销毁的时候,清除所有的定时任务。 addCronTask(Runnable, String) 方法用来添加一个定时任务,传两个参数,第一个是 Runnable,也就是我们前面所说的定时任务,第二个则是一个 Cron 表达式。 addCronTask(CronTask) 方法也用来添加定时任务,添加之前先判断这个定时任务是否已经存在,如果已经存在,就先移除。然后将定时任务存入 scheduledTasks 中,存储的时候,key 就是那个 Runnable 对象,value 则是一个 ScheduledTask 对象。 ScheduledTask 对象从 scheduleCronTask 方法中获取, 这也是整个系统最最核心的一段代码 ,调用 taskScheduler 对象把定时任务添加进去。 removeCronTask 方法用来移除一个定时任务,移除分为两部分:1. 从 scheduledTasks 集合中找到定时任务并移除;2. 取消定时任务的执行。 最后的 destroy 方法就是一个常规方法,该移除移除,该清空清空。 3.6 InitTask
这是一个处理数据库中已有定时任务的类。当系统启动时,首先从数据库中读取需要定时执行的任务,然后挨个加入定时任务执行器中: @Component public class InitTask implements CommandLineRunner { @Autowired CronTaskRegistrar cronTaskRegistrar; @Autowired SysJobService sysJobService; @Override public void run(String... args) throws Exception { List list = sysJobService.getJobsByStatus(1); for (SysJob sysJob : list) { cronTaskRegistrar.addCronTask(new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams()), sysJob.getCronExpression()); } } } 查询所有状态为 1 的定时任务。 遍历第一步查询出来的集合,添加定时任务。
好啦,这就是整个项目最最核心的配置了,其他的代码都是一些业务层面的代码,乏善可陈,我就不啰嗦啦。 4. 定时任务怎么配
有的小伙伴可能还不知道定时任务怎么配置,我这里稍微说两句。
项目中提供了如下一个测试类: @Component("schedulingTaskDemo") public class SchedulingTaskDemo { public void taskWithParams(String params) { System.out.println("执行有参示例任务:" + params); } public void taskNoParams() { System.out.println("执行无参示例任务"); } }
这是提前写好的,需要的时候我们配置的定时任务就是这里相关的参数,如下图:
Bean 名称、方法名称都和测试案例中的 Bean 一一对应。 5. 小结
好啦,是不是很 Easy?小伙伴们赶紧去尝试下吧!
项目地址: GitHub:https://github.com/lenve/scheduling Gitee:https://gitee.com/lenve/scheduling 原文链接:https://mp.weixin.qq.com/s/skZ7uU7q1iH9QrV2EKiGOg
原作者:江南一点雨
频现拼写翻译等内容错误莫让词典类APP误人子弟在手机APP上查单词背单词,已成为当下大学生中学生及英语学习爱好者的习惯,APP上的电子词典几乎取代了大部头纸质词典。但新华视点记者调查发现,不少动辄用户使用量过亿的英语学习类AP
我今天也学会用支付宝了在当下这个信息飞速发展的时代,信息化以更快更便捷的方式,传递着人类创造文明的发展。同时,也给人类提供了快捷的交往手段,推动了人类生活的共同富裕和繁荣。自从有了手机后,它不仅仅是通话
海雀虽小,五脏俱全华为智选HQ5S海雀AI全景摄像头体验在智能家居高速发展的今天,人们除了享受AI智能带来的方便的同时,更多人开始注重起了家庭隐私及安全问题,从而也诞生了很多保护家庭安全的科技数码产品,在没有宝宝之前,我对智能摄像头从来
腾讯司庆发放NFT藏品,它到底有什么作用?记者司林威11月11日,腾讯公司为其员工发放了23周年纪念版NFT。根据界面新闻了解,腾讯官方将其称为数字藏品,由腾讯旗下NFT交易平台幻核团队设计发行,腾讯旗下联盟链至信链提供链
华为手机怎么设置视频铃声?华为EMUI9。1就增加了视频铃声的功能,接下来我们来看看怎么设置视频铃声吧。1打开华为手机的设置声音,选择卡1或卡2的来电铃声进行设置。如下图所示2我们进入选择铃声的页面中,点击
安防监控摄像机云储存如何开通,可以保存多少视频云储存是网络WiFi摄像机独有的功能,它是利用互联网视频数据传输到网盘,从而达到大容量存储的目的。也就是说没有WiFi的环境是使用不了云盘功能的,只能使用本地存储。安防监控摄像机的
iPhone13pro和华为P50pro应该买哪个?iPhone13pro,是今年整个苹果旗舰系列比较超值的一款手机,整体配置相比于苹果13标准版升级幅度巨大,采用的是a15芯片,GPU性能提升20。但iPhone13pro的价格比
需要打击电商吗?首先要搞清楚电商和实体店的区别在那些方面。我觉得最大的区别是实体店让富裕了房东,而电商是富裕了平台。电商主要是因为互联网的普及,应对市场需求顾客群体量大辐射广,利用价格战营销手段多
为什么华为手机能站稳高端,别的国产手机就不能?谢邀。原因其实很简单,持续增长的科技投入和不断巩固的品牌建设。不仅手机,华为进入哪个行业都要成为第一的这股蛮劲啊,你想让它不成为高端都难。其他的为什么不能就不说了,我都没用过它们的
10月新发布手机性能排行榜vivoT1上榜近日,根据多家科技媒体的消息,鲁大师数据中心公布了10月安卓新发布手机性能排行榜,数据来自鲁大师APP10。01日10。31日的数据,榜单只筛选出在这期间新发布的机型。部分新机测试
iPhone13Pro最新售价确认,256GB版跌至新低价,iPhone13不香了如果购买的是普通安卓手机,使用个两年可能就会考虑更换新机了,但如果使用的是苹果手机,可能会持续使用四五年,因为长期使用的情况下,苹果手机确实要比安卓手机更为流畅,这也是不少消费者们