实现目标:
(1)可动态实现调度,包括动态创建、更新、删除、立即执行、暂停、恢复等调度方式;
(2)分布式部署,实现负载均衡;
(a)多节点部署,同一时间同一定时任务只会有一个节点执行;
(b)多节点部署,在其中一个节点修改了job的执行时间,则对此job执行时间的修改其他节点同样生效;
(c) 多节点部署,其中某个节点down掉,job会自动切换到其他节点执行;同样,若新增节点进来,则新节点会自动分担其他节点执行job的压力;
(3)可作为独立构件集成至其他项目;
项目环境:
springboot 2.0.0.RELEASE
quartz 2.2.1
quartz-jobs 2.2.1
tk.mybatis 2.2.0
mysql-connector-java 5.1.45
HikariCP 2.7.8
核心思路说明:
1、quartz配置
@Configuration
public class ScheduleConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
//quartz参数
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "xxxxScheduler");//实例名称可自定义
prop.put("org.quartz.scheduler.instanceId", "AUTO");
//线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
prop.put("org.quartz.threadPool.threadPriority", "5");
//JobStore配置
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
//集群配置
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.txIsolationLevelReadCommitted", " true");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");//与数据库表名前缀相对应
factory.setQuartzProperties(prop);
factory.setSchedulerName("xxxxcheduler");
factory.setStartupDelay(15);//项目启动后15开始执行定时任务
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
factory.setOverwriteExistingJobs(true);
factory.setAutoStartup(true);//设置自动启动,默认为true
return factory;
}
}
2、数据源配置
@Configuration
@MapperScan(value = "tk.mybatis.mapper.annotation",
basePackages = "cn.xxxx.app.standard.component.quartz.mapper",
markerInterface = JobMapper.class,
mapperHelperRef = "quartzMapperHelper",
sqlSessionTemplateRef = "quartzSqlSessionTemplate"
)
public class QuartzDataSourceConfig {
@Bean(name = "quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz")
public DataSource setDataSource() {
return new HikariDataSource();
}
@Bean(name = "quartzTransactionManager")
public DataSourceTransactionManager setTransactionManager(@Qualifier("quartzDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "quartSqlSessionFactory")
public SqlSessionFactory setSqlSessionFactory(@Qualifier("quartzDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setTypeAliasesPackage("cn.xxxx.app.standard.component.quartz.model.dbo");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml"));
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
conf.setMapUnderscoreToCamelCase(true);
conf.setLogImpl(StdOutImpl.class);
bean.setConfiguration(conf);
return bean.getObject();
}
@Bean(name = "quartzSqlSessionTemplate")
public SqlSessionTemplate setSqlSessionTemplate(@Qualifier("quartSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public MapperHelper quartzMapperHelper() {
Config config = new Config();
List mappers = new ArrayList();
mappers.add(JobMapper.class);
config.setMappers(mappers);
MapperHelper mapperHelper = new MapperHelper();
mapperHelper.setConfig(config);
return mapperHelper;
}
}
3、定时器反射配置
public class ScheduleRunnable implements Runnable {
private Object target;
private Method method;
private String params;
public ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
this.target = SpringContextUtils.getBean(beanName);
this.params = params;
if(!StringUtils.isEmpty(params)){
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
}else{
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
public void run() {
try {
ReflectionUtils.makeAccessible(method);
if(!StringUtils.isEmpty(params)){
method.invoke(target, params);
}else{
method.invoke(target);
}
}catch (Exception e) {
throw new RRException("执行定时任务失败", e);
}
}
}
4、定时器执行类,集成QuartzJobBean
public class ScheduleTask extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
private ExecutorService service = Executors.newSingleThreadExecutor();
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String jsonJob = context.getMergedJobDataMap().getString(ScheduleJob.JOB_PARAM_KEY);
ScheduleJob scheduleJob = new Gson().fromJson(jsonJob, ScheduleJob.class);
//获取spring bean
ScheduleJobLogService scheduleJobLogService = (ScheduleJobLogService) SpringContextUtils.getBean("scheduleJobLogService");
//数据库保存执行记录
ScheduleJobLog log = new ScheduleJobLog();
log.setJobId(scheduleJob.getJobId());
log.setBeanName(scheduleJob.getBeanName());
log.setMethodName(scheduleJob.getMethodName());
log.setParams(scheduleJob.getParams());
log.setCreateTime(new Date());
//任务开始时间
long startTime = System.currentTimeMillis();
try {
//执行任务
logger.info("任务准备执行,任务ID:" + scheduleJob.getJobId());
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(),
scheduleJob.getMethodName(), scheduleJob.getParams());
Future> future = service.submit(task);
future.get();
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
log.setTimes((int) times);
//任务状态 0:成功 1:失败
log.setStatus(0);
logger.info("任务执行完毕,任务ID:" + scheduleJob.getJobId() + " 总共耗时:" + times + "毫秒");
} catch (Exception e) {
logger.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
log.setTimes((int) times);
//任务状态 0:成功 1:失败
log.setStatus(1);
log.setError(StringUtils.substring(e.toString(), 0, 2000));
} finally {
// 屏蔽日志记录数据库
scheduleJobLogService.save(log);
}
}
}
5、定时器操作类
public class ScheduleUtils {
private final static String JOB_NAME = "TASK_";
/**
* 获取触发器key
*/
public static TriggerKey getTriggerKey(Long jobId) {
return TriggerKey.triggerKey(JOB_NAME + jobId);
}
/**
* 获取jobKey
*/
public static JobKey getJobKey(Long jobId) {
return JobKey.jobKey(JOB_NAME + jobId);
}
/**
* 获取表达式触发器
*/
public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) {
try {
return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
} catch (SchedulerException e) {
throw new RRException("获取定时任务CronTrigger出现异常", e);
}
}
/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleTask.class).withIdentity(getJobKey(scheduleJob.getJobId())).build();
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder).build();
//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY, new Gson().toJson(scheduleJob));
scheduler.scheduleJob(jobDetail, trigger);
//暂停任务
if(scheduleJob.getStatus() == Constant.ScheduleStatus.PAUSE.getValue()){
pauseJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException e) {
throw new RRException("创建定时任务失败", e);
}
}
/**
* 更新定时任务
*/
public static void updateScheduleJob(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId());
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId());
//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//参数
trigger.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY, new Gson().toJson(scheduleJob));
scheduler.rescheduleJob(triggerKey, trigger);
//暂停任务
if(scheduleJob.getStatus() == Constant.ScheduleStatus.PAUSE.getValue()){
pauseJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException e) {
throw new RRException("更新定时任务失败", e);
}
}
/**
* 立即执行任务
*/
public static void run(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
//参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(ScheduleJob.JOB_PARAM_KEY, new Gson().toJson(scheduleJob));
scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
} catch (SchedulerException e) {
throw new RRException("立即执行定时任务失败", e);
}
}
/**
* 暂停任务
*/
public static void pauseJob(Scheduler scheduler, Long jobId) {
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new RRException("暂停定时任务失败", e);
}
}
/**
* 恢复任务
*/
public static void resumeJob(Scheduler scheduler, Long jobId) {
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new RRException("恢复定时任务失败", e);
}
}
/**
* 删除定时任务
*/
public static void deleteScheduleJob(Scheduler scheduler, Long jobId) {
try {
scheduler.deleteJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new RRException("删除定时任务失败", e);
}
}
}
5、spring上下文配置工具类
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static T getBean(String name, Class requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class extends Object> getType(String name) {
return applicationContext.getType(name);
}
}
6、mapper接口
public interface JobMapper extends Mapper {
}
7、自定义异常
public class RRException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public RRException(String msg) {
super(msg);
this.msg = msg;
}
public RRException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public RRException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public RRException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
注意事项:(1)若job(一次性job)已被执行完毕,则无法修改;
(2)quartz采用随机的负载均衡算法,job以随机的方式由不同节点执行;
(3)若当前只有一个job,此job只会由其中某个节点执行;
完整项目请访问:https://github.com/HarlanHu/quartz