SpringBoot集成quartz定时调度任务并通过JDBC持久化
话不多说上干货
xxx项目中有很多定时任务用于生产环境中在特殊时间点执行大批量数据聚合的任务,出于负载均衡的要求,我们的解决方案是对外放开一些接口来调用相关业务逻辑,这就需要接入外部定时调度平台,在该平台上可以自由的配置、测试、执行定时任务并持久存储至数据库中,在系统启动时根据数据库中的任务信息自动的加载任务。
项目pom依赖
<!--SpringBoot父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<!--Mybatis-Plus,但是对于数据库的操作就是一些简单的增删改查,自行换成Mybatis或者其他框架都行-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!--德鲁伊数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.13</version>
</dependency>
<!--定时作业调度库-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
<exclusions>
<!--由于上面使用德鲁伊数据源,所以quartz自带的c3p0最好排除掉-->
<exclusion>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
配置类
/**
* 定时任务调度器配置
* 众所周知,SpringBoot的定时任务默认情况下都是运行在同一个线程内,所以当同一时间点有多个任务
* 的话,就会导致原本希望同一时间并行执行的任务变成了串行执行,所以在设计定时任务时通常希望定时
* 任务触发的业务逻辑代码是异步执行的,所以需要一个线程池。
* 此处使用org.quartz.simpl.SimpleThreadPool提供的线程池,利用反射创建
*/
@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", "MyScheduler");
//调度器ID
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.maxMisfiresToHandleAtATime", "1");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
factory.setQuartzProperties(prop);
factory.setSchedulerName("RenrenScheduler");
//延时启动
factory.setStartupDelay(30);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
//QuartzScheduler 启动时更新己存在的Job
factory.setOverwriteExistingJobs(true);
//设置自动启动,默认为true
factory.setAutoStartup(true);
return factory;
}
}
抽象出调度任务实体类
抽象实体类
@Data
@TableName("job")
public class ScheduleJobEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 追溯源码发现,在org.quartz.utils.DirtyFlagMap中的DirtyFlagMap方法(77行)中,
* 调度任务的参数getJobDataMap实质上是一个HashMap,在这个体系中,约定所有调度任务
* 的参数都是一个JSON字符串,这就需要一个公用的KEY保证在后续过程中能拿出作为参数的JSON
* 字符串
* 任务调度参数key
*/
public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";
/**
* 任务id
*/
@TableId
private Long jobId;
/**
* 调度接口地址,一般为开放接口
*/
private String httpUri;
/**
* 参数,JSON字符串
*/
private String params;
/**
* cron表达式
*/
@NotBlank(message="cron表达式不能为空")
private String cronExpression;
/**
* 任务状态
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
}
数据库表脚本
CREATE TABLE `job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`bean_name` varchar(200) DEFAULT NULL COMMENT '调度接口地址,一般为开放接口',
`params` varchar(2000) DEFAULT NULL COMMENT '参数,JSON字符串',
`cron_expression` varchar(100) DEFAULT NULL COMMENT 'cron表达式',
`status` tinyint(4) DEFAULT NULL COMMENT '任务状态 0:正常 1:暂停',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='定时任务';
调度执行和调度任务更改工具类
调度任务实际要执行的业务代码,这里是访问一个指定的外部接口
public class ScheduleJob extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ScheduleJobEntity scheduleJobEntity = (ScheduleJobEntity) context.getMergedJobDataMap().get(ScheduleJobEntity.JOB_PARAM_KEY);
String httpUri = scheduleJobEntity.getHttpUri();
String params = scheduleJobEntity.getParams();
try {
//执行任务
logger.debug("任务准备执行,任务ID:" + scheduleJobEntity.getJobId());
JSONObject jsonObject = HttpClientUtil.doHttpPost(httpUri, params);
/**
* 这里也可以根据接口响应的JSONObject判断定时调度成功没
* 作为演示不再展示
*/
} catch (Exception e) {
logger.error("任务执行失败,任务ID:" + scheduleJobEntity.getJobId(), e);
}
}
}
从传入的Scheduler中构建、更改、动态删除定时任务工具类
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, ScheduleJobEntity scheduleJob) {
try {
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.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(ScheduleJobEntity.JOB_PARAM_KEY, 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, ScheduleJobEntity 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(ScheduleJobEntity.JOB_PARAM_KEY, 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, ScheduleJobEntity scheduleJob) {
try {
//参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(ScheduleJobEntity.JOB_PARAM_KEY, 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);
}
}
}
HttpClient连接工具类,提供访问外部接口的方法
public class HttpClientUtil {
private static CloseableHttpClient httpClient = null;
static {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(150);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(1000)
.setConnectionRequestTimeout(2000)
.setSocketTimeout(3000)
.build();
HttpRequestRetryHandler retryHandler = new StandardHttpRequestRetryHandler();
httpClient = HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig)
.setRetryHandler(retryHandler).build();
}
public static JSONObject doHttpPost(String uri, String json) {
CloseableHttpResponse response = null;
try {
HttpPost httpPost = new HttpPost(uri);
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
StringEntity stringEntity = new StringEntity(json,"utf-8");
httpPost.setEntity(stringEntity);
response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
if (HttpStatus.SC_OK == statusCode) {
HttpEntity entity = response.getEntity();
if (null != entity) {
String resStr = EntityUtils.toString(entity, "utf-8");
return JSON.parseObject(resStr);
}
}
} catch (Exception e) {
log.error("CloseableHttpClient-post-请求异常", e);
} finally {
try {
if (null != response) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return new JSONObject();
}
}
调度配置与执行的代码完毕,下面就是对持久化到数据库中的调度任务数据进行操作
与日常CRUD一样,先创建一个有CRUD方法的接口
//这里由于Mybatis-Plus的原因继承IService,但是只是想用生成的crud方法,与核心调度没关系,使用中自行更换
public interface ScheduleJobService extends IService<ScheduleJobEntity> {
PageUtils queryPage(Map<String, Object> params);
/**
* 保存定时任务
*/
void saveJob(ScheduleJobEntity scheduleJob);
/**
* 更新定时任务
*/
void update(ScheduleJobEntity scheduleJob);
/**
* 批量删除定时任务
*/
void deleteBatch(Long[] jobIds);
/**
* 批量更新定时任务状态
*/
int updateBatch(Long[] jobIds, int status);
/**
* 立即执行
*/
void run(Long[] jobIds);
/**
* 暂停运行
*/
void pause(Long[] jobIds);
/**
* 恢复运行
*/
void resume(Long[] jobIds);
}
重点实现类来了
@Service("scheduleJobService")
public class ScheduleJobServiceImpl extends ServiceImpl<ScheduleJobDao, ScheduleJobEntity> implements ScheduleJobService {
/**
* 这个调度主面板是用配置类方式注入容器的,是一种单例,系统运行期间就一个
* 所以可以使用上面定义的工具类,将这个容器中的示例传进去实现对内部调度数据
* 的更改
*/
@Autowired
private Scheduler scheduler;
/**
* 项目启动时,初始化定时器
* 从数据库中查出配置过得定时任务加入定时器
* 就实现了调度任务的持久化
*/
@PostConstruct
public void init() {
List<ScheduleJobEntity> scheduleJobList = this.list();
for (ScheduleJobEntity scheduleJob : scheduleJobList) {
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
//如果不存在,则创建
if (cronTrigger == null) {
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
} else {
ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
}
}
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
String beanName = (String) params.get("beanName");
IPage<ScheduleJobEntity> page = this.page(
new Query<ScheduleJobEntity>().getPage(params),
new QueryWrapper<ScheduleJobEntity>().like(StringUtils.isNotBlank(beanName), "bean_name", beanName)
);
return new PageUtils(page);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveJob(ScheduleJobEntity scheduleJob) {
scheduleJob.setCreateTime(new Date());
scheduleJob.setStatus(Constant.ScheduleStatus.NORMAL.getValue());
this.save(scheduleJob);
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(ScheduleJobEntity scheduleJob) {
ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
this.updateById(scheduleJob);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteBatch(Long[] jobIds) {
for (Long jobId : jobIds) {
ScheduleUtils.deleteScheduleJob(scheduler, jobId);
}
//删除数据
this.removeByIds(Arrays.asList(jobIds));
}
@Override
public int updateBatch(Long[] jobIds, int status) {
Map<String, Object> map = new HashMap<>(2);
map.put("list", jobIds);
map.put("status", status);
return baseMapper.updateBatch(map);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void run(Long[] jobIds) {
for (Long jobId : jobIds) {
ScheduleUtils.run(scheduler, this.getById(jobId));
}
}
/**
* 暂停任务
* @param jobIds
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void pause(Long[] jobIds) {
for (Long jobId : jobIds) {
ScheduleUtils.pauseJob(scheduler, jobId);
}
updateBatch(jobIds, Constant.ScheduleStatus.PAUSE.getValue());
}
/**
* 恢复任务
* @param jobIds
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void resume(Long[] jobIds) {
for (Long jobId : jobIds) {
ScheduleUtils.resumeJob(scheduler, jobId);
}
updateBatch(jobIds, Constant.ScheduleStatus.NORMAL.getValue());
}
}
下面,自行来一个控制器接受页面请求,注入ScheduleJobService开始对定时任务的各种操作
例子:
@RestController
public class TaskJobController {
private ScheduleJobService scheduleJobService;
@PostMapping(value = "/add")
public Result add(@RequestBody ScheduleJobEntity scheduleJob){
scheduleJobService.saveJob(scheduleJob);
Result success = Result.success();
return success;
}
public static class Result {
private String resultCode;
private String resultMessage;
private Map<String,Object> params = new HashMap<String, Object>();
public static Result success(){
Result result = new Result();
result.setResultCode("200");
result.setResultMessage("success");
return result;
}
public static Result failed(){
Result result = new Result();
result.setResultCode("500");
result.setResultMessage("failed");
return result;
}
public String getResultCode() {
return resultCode;
}
public String getResultMessage() {
return resultMessage;
}
public Map<String, Object> getParams() {
return params;
}
public void setResultCode(String resultCode) {
this.resultCode = resultCode;
}
public void setResultMessage(String resultMessage) {
this.resultMessage = resultMessage;
}
@Override
public String toString() {
return "Result{" +
"resultCode='" + resultCode + '\'' +
", resultMessage='" + resultMessage + '\'' +
", params=" + params +
'}';
}
}
}