背景
公司现有项目为基于Quartz实现的定时任务的项目由于架构的要求需要对当前架构进行调整 简单介绍当下的情况就是项目中有两个服务模块:manage和task。
最初的规划是manage负责基于Quartz框架最基础的API对前端提供任务管理的接口,当前模块不开启任务的调度;
task模块中只进行任务的执行和调度,不参与其他业务的实现。而具体的任务全部抽出到一个新的bean模块中(当前bean不为服务),然后manage和task都会依赖于这个bean。
- 线上部署方案:
- Manage服务:单节点部署(无状态)
- Task服务:可弹性伸缩的集群部署
当前架构痛点
按照当前架构开发并且运行一段时间后发现问题如下:
- manage服务其实只对外提供了Quartz管理任务的基础API,并没有进行任务的调度但是还需要引入很多quartz相关依赖以及bean模块,这样就导致项目结构很混乱
- 由于bean处于一个被两个服务依赖的情况,即使已经将bean模块中所需的配置已经单独抽出一个配置文件但还是需要分别在manage和task中重复引入。
- 开发完任务的具体实现在测试阶段很多时候只走了manage模块的场景,遗漏task中是否可以正常
架构优化
综上所述做出如下修改
- 移除manage中所有quartz以及bean模块依赖
- 将bean模块移入task服务中,并且将manage中的quartz任务API迁移至task服务中
- manage服务通过feign调用task接口继续向前端提供任务维护接口
架构简图对比
旧架构: [Manage] --> (依赖Quartz+Bean) [Task] --> (依赖Quartz+Bean) Bean模块独立
新架构: [Manage] --> [Feign] --> [Task] Task内部包含Bean模块
迁移问题
理论上来讲此次调整只需要将bean中的代码直接放到task服务中,并且将manage中的相关依赖删除掉即可。不出意外的话意外就出现了,运行日志中提示quartz中Properties的org.quartz.jobStore.class配置:org.springframework.scheduling.quartz.JobStoreTX
不存在
通过排查发现对应的JobStoreTX类,但是我并没有修改quartz依赖的版本,也就是说之前设置的配置有可能全部没有起作用,由于quartz的配置通过spring bean的方式进行设置并没有发现,具体配置如下:
@Configuration
@Import(DataSourceConfiguration.class)
public class QuartzConfiguration implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(QuartzConfiguration.class);
// =================调整架构后新增配置=================
private ApplicationContext applicationContext;
@Resource
private Scheduler scheduler;
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@EventListener
public void onApplicationEvent(@Nonnull WebServerInitializedEvent event) {
this.loadQuartzJobListener();
this.loadQuartzTriggerListener();
this.loadQuartzJobBean();
}
private void loadQuartzJobListener() {
Map<String, QuartzJobListenerTemplate> jobListenerMap =
applicationContext.getBeansOfType(QuartzJobListenerTemplate.class);
jobListenerMap.forEach((k, v) -> {
try {
scheduler.getListenerManager().addJobListener(v);
} catch (Exception e) {
logger.error("Initialization QuartzJobListener error", e);
}
});
}
private void loadQuartzTriggerListener() {
Map<String, QuartzTriggerListenerTemplate> triggerListenerMap =
applicationContext.getBeansOfType(QuartzTriggerListenerTemplate.class);
triggerListenerMap.forEach((k, v) -> {
try {
scheduler.getListenerManager().addTriggerListener(v);
} catch (Exception e) {
logger.error("Initialization QuartzTriggerListener error", e);
}
});
}
private void loadQuartzJobBean() {
Map<String, Object> quartzJobMap = applicationContext.getBeansWithAnnotation(QuartzJob.class);
Map<String, String> quatrzBeanMap = MiddlewareDataManage.getInstance().getQuartzBeanMap();
quartzJobMap.forEach((k, v) -> quatrzBeanMap.put(k, v.getClass().getName()));
}
// =================调整架构前的原配置=================
@Autowired
@Qualifier(DataSourceConfiguration.DS_BEAN_NAME)
private DataSource dataSource;
@Autowired
@Qualifier(DataSourceConfiguration.TRAN_MGR_BEAN_NAME)
private DataSourceTransactionManager dataSourceTransactionManager;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.setStartupDelay(1);
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setJobFactory(springBeanJobFactory());
schedulerFactoryBean.setDataSource(dataSource);
schedulerFactoryBean.setTransactionManager(dataSourceTransactionManager);
schedulerFactoryBean.setQuartzProperties(quartzProperties());
return schedulerFactoryBean;
}
@Bean
public SpringBeanJobFactory springBeanJobFactory() {
// 必须指定JobFactory,否则Job类中的Service无法注入
return new SpringBeanJobFactory();
}
@Bean
public Properties quartzProperties() {
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "flight-monitor-task-service");
properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");
properties.setProperty("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
properties.setProperty("org.quartz.jobStore.driverDelegateClass",
"org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
properties.setProperty("org.quartz.jobStore.tablePrefix", "qrtz_");
properties.setProperty("org.quartz.jobStore.isClustered", "true");
properties.setProperty("org.quartz.jobStore.misfireThreshold", "2000");
properties.setProperty("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
properties.setProperty("org.quartz.jobStore.clusterCheckinInterval", "15000");
properties.setProperty("org.quartz.jobStore.useProperties", "false");
properties.setProperty("org.quartz.threadPool.threadCount", "30");
properties.setProperty("org.quartz.threadPool.threadPriority", "5");
return properties;
}
}
查询quartz的文档发现JobStoreTX类是低版本所用的,我们项目中的版本为2.x应该使用LocalDataSourceJobStore.class,调整后继续报错提示:No local DataSource found for configuration - 'dataSource' property must be set on SchedulerFactoryBean
在上面给出的配置中可以看到我已经在schedulerFactoryBean方法中设置了DataSource以及dataSourceTransactionManager,但是为什么还提示数据源为空呢?debug后发现确实为null,可以确认的是在迁移前配置是可用的,但是增加监听器的相关配置后却出现了数据源无法获取的问题,应该是配置类增加了ApplicationContextAware实现影响了spring bean依赖的加载顺序
将其改为setter方式进行注册,依然为null
于是我又修改为通过方法参数进行注入,发现成功运行图中的null是数据源的线程池name为空,原因是当前线程池还未完全初始化完成。
遗留问题解答补充
程序虽然启动起来了但是仍然遗留了两个问题
-
为什么架构调整前quartz版本没有变化的情况下,旧架构JobStoreTX配置有效?
于是回到之前的配置重新dubug排查,配置如下
进入到SchedulerFactoryBean类中发现,定位到afterPropertiesSet方法中,其中的对Scheduler实例进行初始化
进入prepareSchedulerFactory方法的initSchedulerFactory方法
其中关键代码为此处的数据源判断,如果数据源不为空会自动将org.quartz.jobStore.class配置设置为LocalDataSourceJobStore类,也就是说调整架构之前的写法一直为其作用,走的默认值
- Spring的
SchedulerFactoryBean在检测到setDataSource()被调用时 - 自动将
org.quartz.jobStore.class设置为LocalDataSourceJobStore - 这个行为优先级高于配置文件中的显式设置
- 实际运行时使用的是正确的JobStore实现
- 导致旧配置中的错误值
JobStoreTX被静默覆盖
- ApplicationContextAware的影响
Spring容器初始化Bean时会通过 BeanPostProcessor 处理 ApplicationContextAware 接口,强制调用 setApplicationContext() 方法。这会使得 Configuration 类在更早的阶段被初始化(早于普通 Bean 的依赖注入阶段)。此时,其他 Bean 可能尚未完成初始化,导致通过 Setter 或字段注入(@Autowired)的方式无法获取到正确的依赖值。
而通过 @Bean 方法参数注入是延迟解析的。Spring 在调用 @Bean 方法时,会实时解析参数对应的 Bean,此时所有依赖的 Bean 已经初始化完成。