本文参考了网络资料(均以超链接标注),对于其是否为原创,本文不付任何责任。
Quartz 是比较常用的任务调度框架,关于基础使用请参考Documentation。本文主要就实际运用中涉及到的动态加载和集群配置做介绍。
动态任务
动态任务的核心思路是将XML配置转换为管理类统一管理。
quartz-config.xml 如下
<bean id="schedulerBeanFactory" class="xxx.QuartzSpringJobFactory"/>
<!-- factory -->
<bean id="schedulerFactory"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="autoStartup" value="true" />
</bean>
<bean id="quartzJobService" class="xxx.QuartzJobServiceImpl"/>
QuartzSpringJobFactory.class
public class QuartzSpringJobFactory extends SpringBeanJobFactory{
@Autowired
private AutowireCapableBeanFactory beanFactory;
/**
* 这里我们覆盖了super的createJobInstance方法,对其创建出来的类再进行autowire。
*/
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object jobInstance = super.createJobInstance(bundle);
beanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
QuartzJobService.class 实现类 QuartzJobServiceImpl
package xxx.impl;
import java.util.Calendar;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import xxx.ClusterQuartzJob;
import xxx.QuartzJob;
import xxx.QuartzJobService;
public class QuartzJobServiceImpl implements QuartzJobService{
private Logger logger = LoggerFactory.getLogger(QuartzJobServiceImpl.class);
@Autowired
private SchedulerFactoryBean schedulerFactory;
@Autowired
private BeanFactory schedulerBeanFactory;
@Override
public JobDetail getJobDetail(String name, String group) {
JobKey jobKey = new JobKey(name, group);
try {
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(jobKey);
return jobDetail;
} catch (SchedulerException e) {
this.logger.error("{}",e);
}
return null;
}
@Override
public void addJob(QuartzJob job) {
try {
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(new JobKey(job.getName(), job.getGroupCode()));
if (null != jobDetail) {
throw new JobExecutionException("Group:" + job.getGroupCode() + ", name:" + job.getName() + ". Job already exist.");
}
jobDetail = this.getJobDetail(job);
if (null != jobDetail) {
CronTrigger cronTrigger = this.getCronTrigger(job,jobDetail);
schedulerFactory.getScheduler().addJob(jobDetail, true);
schedulerFactory.getScheduler().scheduleJob(cronTrigger);
if (job.getStartNow() && null == job.getStartTime() && Calendar.getInstance().getTime().after(job.getStartTime())) {
cronTrigger.getTriggerBuilder().startNow();
}
}
} catch (Exception e) {
this.logger.error("{}",e);
}
}
@Override
public void removeJob(QuartzJob job) {
try {
JobKey jobKey = new JobKey(job.getName(), job.getGroupCode());
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(jobKey);
if (null == jobDetail) {
throw new JobExecutionException("Group:" + job.getGroupCode() + ", name:" + job.getName() + ". Job not exist.");
}
schedulerFactory.getScheduler().pauseTrigger(new TriggerKey(job.getName(), job.getGroupCode()));
schedulerFactory.getScheduler().deleteJob(jobKey);
} catch (Exception e) {
this.logger.error("{}",e);
}
}
@Override
public void stopJob(QuartzJob job) {
try {
JobKey jobKey = new JobKey(job.getName(), job.getGroupCode());
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(jobKey);
if (null == jobDetail) {
throw new JobExecutionException("Group:" + job.getGroupCode() + ", name:" + job.getName() + ". Job not exist.");
}
schedulerFactory.getScheduler().pauseTrigger(new TriggerKey(job.getName(), job.getGroupCode()));
schedulerFactory.getScheduler().pauseJob(jobKey);
} catch (Exception e) {
this.logger.error("{}",e);
}
}
@Override
public void resumeJob(QuartzJob job) {
try {
JobKey jobKey = new JobKey(job.getName(), job.getGroupCode());
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(jobKey);
if (null == jobDetail) {
throw new JobExecutionException("Group:" + job.getGroupCode() + ", name:" + job.getName() + ". Job not exist.");
}
schedulerFactory.getScheduler().resumeJob(jobKey);
schedulerFactory.getScheduler().resumeTrigger(new TriggerKey(job.getName(), job.getGroupCode()));
} catch (Exception e) {
this.logger.error("{}",e);
}
}
@Override
public void reschedule(QuartzJob job) {
try {
JobKey jobKey = new JobKey(job.getName(), job.getGroupCode());
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(jobKey);
if (null == jobDetail) {
throw new JobExecutionException("Group:" + job.getGroupCode() + ", name:" + job.getName() + ". Job not exist.");
}
TriggerKey triggerKey = new TriggerKey(job.getName(), job.getGroupCode());
CronTrigger newTrigger = this.getCronTrigger(job,jobDetail);
schedulerFactory.getScheduler().rescheduleJob(triggerKey, newTrigger);
if (job.getStartNow() && null != job.getStartTime() && Calendar.getInstance().getTime().after(job.getStartTime())) {
newTrigger.getTriggerBuilder().startNow();
}
} catch (Exception e) {
this.logger.error("{}",e);
}
}
private JobDetail getJobDetail(QuartzJob job){
try {
MethodInvokingJobDetailFactoryBean jobDetailFactoryBean = new MethodInvokingJobDetailFactoryBean();
jobDetailFactoryBean.setGroup(job.getGroupCode());
jobDetailFactoryBean.setName(job.getName());
jobDetailFactoryBean.setBeanFactory(this.schedulerBeanFactory);
jobDetailFactoryBean.setTargetBeanName(job.getBeanName());
jobDetailFactoryBean.setTargetMethod(job.getExecuteMethod());
jobDetailFactoryBean.setConcurrent(job.getConcurrent());
jobDetailFactoryBean.afterPropertiesSet();
return jobDetailFactoryBean.getObject();
} catch (Exception e) {
this.logger.error("{}",e);
return null;
}
}
private CronTrigger getCronTrigger(QuartzJob job, JobDetail jobDetail){
CronTriggerFactoryBean cronTrigger = new CronTriggerFactoryBean();
cronTrigger.setGroup(job.getGroupCode());
cronTrigger.setName(job.getName());
cronTrigger.setJobDetail(jobDetail);
cronTrigger.setCronExpression(job.getCronExpression());
try {
cronTrigger.afterPropertiesSet();
} catch (Exception e) {
this.logger.error("{}",e);
}
if (null != job.getStartTime()) {
cronTrigger.getObject().getTriggerBuilder().startAt(job.getStartTime());
}
if (null != job.getEndTime()) {
cronTrigger.getObject().getTriggerBuilder().endAt(job.getEndTime());
}
return cronTrigger.getObject();
}
}
用到的几个基础类
public class QuartzJob implements Serializable{
/**
*
*/
private static final long serialVersionUID = -1007495492595068279L;
private Long id;
private String groupCode;
private String name;
private String beanName;
private String executeMethod;
private Object[] param;//执行任务方法的参数
private Date startTime;
private Date endTime;
private String cronExpression;
private Boolean concurrent = Boolean.FALSE;
private Boolean startNow = Boolean.FALSE;
private QuartzJobStatus status;
public Object[] getParam() {
return param;
}
public void setParam(Object[] param) {
this.param = param;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getGroupCode() {
return groupCode;
}
public String getName() {
return name;
}
public String getBeanName() {
return beanName;
}
public String getExecuteMethod() {
return executeMethod;
}
public Date getStartTime() {
return startTime;
}
public Date getEndTime() {
return endTime;
}
public String getCronExpression() {
return cronExpression;
}
public Boolean getConcurrent() {
return concurrent;
}
public Boolean getStartNow() {
return startNow;
}
public QuartzJobStatus getStatus() {
return status;
}
public void setGroupCode(String groupCode) {
this.groupCode = groupCode;
}
public void setName(String name) {
this.name = name;
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public void setExecuteMethod(String executeMethod) {
this.executeMethod = executeMethod;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public void setCronExpression(String cronExpression) {
this.cronExpression = cronExpression;
}
public void setConcurrent(Boolean concurrent) {
this.concurrent = concurrent;
}
public void setStartNow(Boolean startNow) {
this.startNow = startNow;
}
public void setStatus(QuartzJobStatus status) {
this.status = status;
}
}
public enum QuartzJobStatus {
DEFAULT(-1),RUNNING(0),STOPPED(1),DELETED(9);
private final Integer value;
private QuartzJobStatus(Integer value){
this.value = value;
}
public Integer getValue() {
return value;
}
public static QuartzJobStatus get(Integer value) {
for (QuartzJobStatus status : QuartzJobStatus.values()) {
if (status.getValue().intValue() == value.intValue()) {
return status;
}
}
return DEFAULT;
}
}
业务系统独立封装quartz的db配置,通过QuartzJob类传递配置,动态控制quartz 任务。
集群
PART 1 已经介绍了动态控制,但是如果应用的微服务等分布式服务中,就会出现任务重复执行等一系列问题。
quartz本身支持分布式配置,Spring整合Quartz定时任务 在集群、分布式系统中的应用 详细介绍了配置。其中涉及到的sql脚本可以通过官网下载获得。(注:2.1x和2.2x有所不同,qrtz_fired_triggers 中多了SCHED_TIME BIGINT(13))。
修改配置后,启动项目会遇到 MethodInvokingJobDetailFactoryBean 无法序列化的bug,SPR-3797。网上很多转载均是已copy了此bug的解决方案。Spring 4.1 + 版本,MethodInvokingJobDetailFactoryBean已经有所调整,所以不再适用此解决方案。
@Override
@SuppressWarnings("unchecked")
public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
prepare();
// Use specific name if given, else fall back to bean name.
String name = (this.name != null ? this.name : this.beanName);
// Consider the concurrent flag to choose between stateful and stateless job.
Class<?> jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class);
// Build JobDetail instance.
JobDetailImpl jdi = new JobDetailImpl();
jdi.setName(name);
jdi.setGroup(this.group);
jdi.setJobClass((Class) jobClass);
jdi.setDurability(true);
jdi.getJobDataMap().put("methodInvoker", this);
this.jobDetail = jdi;
postProcessJobDetail(this.jobDetail);
}
如上图,Spring4.1+版本已经修改了afterPropertiesSet方法,且 new JobDetail 在quartz2.2x版本已不存在。所以此方案无效。
spring集成quartz中java.lang.ClassNotFoundException: org.quartz.impl.JobDetailImpl异常解决方法 中具体描述了这个问题。
二、集群环境下定时调度的解决方案之Quartz集群 提供了一种解决方案,测试可行。
(待补充。该方案重写 execute 时,尚未测试与spring的集成)
************ 2018/9/5 补充************
quartz2.2x之后,可以借助JobDataMap进行参数传递。
新增自定义quartz job类
package xxx.quartz;
import java.lang.reflect.Method;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;
public class ClusterQuartzJob implements Job {
private Logger logger = LoggerFactory.getLogger(ClusterQuartzJob.class);
public static final String BEAN_NAME = "beanName";
public static final String METHOD_NAME = "methodName";
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
JobDataMap jobDataMap = context.getTrigger().getJobDataMap();
ApplicationContext ctx = this.getApplicationContext(context);
Object bean = ctx.getBean(jobDataMap.getString(ClusterQuartzJob.BEAN_NAME));
Method method = ReflectionUtils.findMethod(bean.getClass(), jobDataMap.getString(ClusterQuartzJob.METHOD_NAME));
ReflectionUtils.invokeMethod(method, bean);
} catch (Exception e) {
this.logger.error("{}",e);
}
}
private static final String APPLICATION_CONTEXT_KEY = "applicationContextKey";
private ApplicationContext getApplicationContext(JobExecutionContext context) throws Exception {
ApplicationContext appCtx = null;
appCtx = (ApplicationContext) context.getScheduler().getContext().get(APPLICATION_CONTEXT_KEY);
if (appCtx == null) {
throw new JobExecutionException("No application context available in scheduler context for key \"" + APPLICATION_CONTEXT_KEY + "\"");
}
return appCtx;
}
}
任务动态增加时,通过map传递参数。
@Override
public void addClusterJob(QuartzJob job){
try {
JobDetail jobDetail = schedulerFactory.getScheduler().getJobDetail(new JobKey(job.getName(), job.getGroupCode()));
if (null != jobDetail) {
this.logger.info("Group:{}, name:{} already exist.", job.getGroupCode(), job.getName());
this.removeJob(job);
}
Scheduler scheduler = schedulerFactory.getScheduler();
jobDetail = JobBuilder.newJob(ClusterQuartzJob.class)
.withIdentity(job.getName(), job.getGroupCode()).build();
Trigger trigger = TriggerBuilder
.newTrigger()
.withIdentity(job.getExecuteMethod(), job.getGroupCode())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression())).build();
JobDataMap jobDataMap = trigger.getJobDataMap();
jobDataMap.put(ClusterQuartzJob.BEAN_NAME, job.getBeanName());
jobDataMap.put(ClusterQuartzJob.METHOD_NAME, job.getExecuteMethod());
scheduler.scheduleJob(jobDetail, trigger);
if (scheduler.isStarted()) {
scheduler.start();
}
} catch (Exception e) {
this.logger.error("{}",e);
}
}