前言

Java技术栈使用定时任务框架肯定首选Quartz,Quartz支持分布式执行,其原理是借助数据库实现分布式锁竞争保证任务不会同时在多台机器上执行。

本文介绍springboot默认定时任务实现原理,以及在springboot单机模式下如何管理项目中所有定时任务:查看任务列表,动态修改定时cron表达式,删除定时任务,新增定时任务等操作。

之所以对spring定时任务进行分析也跟工作相关,因为公司目前用airflow作为分布式调度管理大数据离线全部跑批任务,但是airflow由python开发如果对python不熟悉的同学不能进行二次开发这个是java技术栈同学的主要痛点,目前国内开源了一款海豚调度功能上与airflow类似同时支持分布式调度与依赖检查并且海豚调度是服务端是java技术栈开发非常适合java技术栈的同学使用并进行二次开发。

值得一提的是海豚调度使用的框架为:Springboot,Quartz,Zookeeper,Mysql等框架进行开发,这些对java技术栈同学来说非常友好,因此部门也在计划使用海豚调度替换Airflow。

介绍

本文重点分析springboot是如何集成spring中定时任务,因为定时任务在spring框架中已经存在很长时间。在springboot项目中使用的默认定时任务同样由spring提供。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

在springboot项目中开启定时任务只需要使用@EnableScheduling注解并在需要定时任务的方法上增加@Scheduled

通过这两个注解可以轻松开启定时任务但是这两个注解背后的逻辑全部由框架实现,如果不熟悉原理的用户使用一旦出现问题将会手忙脚乱不知如何从根源上修改,如果想要更高级的使用定时任务(动态新增定时任务,动态编辑定时任务cron表达式,动态删除定时任务,查看当前定时任务列表)本文从原理到应用进行逐步分析

原理解析

由于springboot通过注解提供默认配置做到开箱即用的便利,但为了能更丝滑的使用还是要了解框架层的逻辑这样才能更好的面向框架编程

在分析@EnableScheduling注解时涉及到springboot是如何扫描beanDefinition启动时如何调用事件监听器以及何时加载spring容器并初始化bean的分析都会直接忽略不然篇幅太长容易跑题。(这三部分内容将会单独写文章进行分析)


先通过一张high-level的流程图进行简单分析,然后进行源码的层层分析。有了总的框架在看源码snippet更加容易理解

 @EnableScheduling注解本质通过@Import注解加载定时任务配置类。此处@Import注解是在springboot启动时将对应类以beanDefinition对象存储到spring的BeanFactory中。

SchedulingConfiguration是连接springboot与spring-context的桥梁。其内部通过初始化后置处理器并注册到spring容器中,等spring进行bean初始化时对bean进行扫描并将带有@Scheduled注解方法进行封装

创建后置处理器 ScheduledAnnotationBeanPostProcessor并创建默认任务注册器ScheduledTaskRegistrar,ScheduledTaskRegistrar非常重要后面所有逻辑都是围绕任务注册器进行参数设置。

通过spring生命周期接口在bean初始化时进行回调并将spring上下文,spring工厂等引用设置到当前对象,然后通过容器上下文刷新事件触发初始化

onApplicationEvent方法初始化,虽然初始化代码比较长但是逻辑相对简单,主要做了三件事:

  • 首先通过beanFactory加载所有SchedulingConfigurer实现类此接口相当重要,通过该接口可以获取到定时任务注册器对象ScheduledTaskRegistrar,该对象是实现动态管理定时任务的基础
  • 其次通过依赖注入方式逐步寻找TaskScheduler实例,首先通过按照类型方式注入,然后通过按照名称方式注入进行多次判断。如果能够找到实例就将其设置到ScheduledTaskRegistrar中,如果没有找到也没关系后面会有默认实现兜底
  • 最后调用注册器将所有定时任务初始化。(此代码判断逻辑通过try catch处理因此略长)

private void finishRegistration() {
	if (this.scheduler != null) {
		this.registrar.setScheduler(this.scheduler);
	}

	if (this.beanFactory instanceof ListableBeanFactory) {
		Map<String, SchedulingConfigurer> beans =
	      ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
		  List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
		  AnnotationAwareOrderComparator.sort(configurers);
		  for (SchedulingConfigurer configurer : configurers) {
			configurer.configureTasks(this.registrar);
		}
	}

	if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
		Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
		try {
			// Search for TaskScheduler bean...
			this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
		}
		catch (NoUniqueBeanDefinitionException ex) {
			logger.trace("Could not find unique TaskScheduler bean", ex);
			try {
				this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
			}
			catch (NoSuchBeanDefinitionException ex2) {
				if (logger.isInfoEnabled()) {
					logger.info("More than one TaskScheduler bean exists within the context, and " +
								"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
								"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
								"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
								ex.getBeanNamesFound());
				}
			}
		}
		catch (NoSuchBeanDefinitionException ex) {
			logger.trace("Could not find default TaskScheduler bean", ex);
			// Search for ScheduledExecutorService bean next...
			try {
				this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
			}
			catch (NoUniqueBeanDefinitionException ex2) {
				logger.trace("Could not find unique ScheduledExecutorService bean", ex2);
				try {
					this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
				}
				catch (NoSuchBeanDefinitionException ex3) {
					if (logger.isInfoEnabled()) {
							logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
									"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
									"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
									"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
									ex2.getBeanNamesFound());
					}
				}
			}
			catch (NoSuchBeanDefinitionException ex2) {
				logger.trace("Could not find default ScheduledExecutorService bean", ex2);
				// Giving up -> falling back to default scheduler within the registrar...
				logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
			}
		}
	}

	this.registrar.afterPropertiesSet();
}

  

 afterPropertiesSet 通过定时器与执行器对任务进行封装并启动。这部逻辑是在注册器中完成已经不在后置处理器中了,但在此之前后置处理器还一件很重要的事情:解析@Schedued注解,解析的方式同样是通过bean生命周期的一个回调函数实现

  

 最终分装定时任务的逻辑还是在ScheduledTaskRegistrar中。

通过层层分析终于知道 @EnableScheduling@Scheduled这两个注解在框架中的逻辑。 

管理定时任务

对定时任务的动态管理在上文其实已经提,依赖SchedulingConfigurer接口。通过自定义配置类实现该接口可以获取到任务注册器。

 任务注册中管理了所有定时任务,我们只需要在业务逻辑的类中将上图中配置类注入后调用ScheduledTaskRegistrar接口即可。例如获取所有定时任务列表,新增任务等

总结

虽然Springboot提供了默认的定时任务,但目前企业应用都是分布式部署显然默认定时任务不能满足分布式环境。因此推荐Quartz作为调度框架进行使用

企业级应用如果使用调度系统尽量与业务系统解耦保证业务系统与调度系统相互隔离。

欢迎感兴趣的同学一起交流。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐