描述
- 系统需要使用到定时任务去完成一些数据处理的工作。
- 希望有一个页面可以控制任务的执行计划,并且记录每次执行的结果,如有异常则记录日志到数据库中。
- quartz官网给出的需要11张表来协同完成,实际中并不需要这么多功能(可能你的项目需要)。
- 我认为两种表就可以完成日常需要的工作,schedule_job(定时任务表),schedule_log(任务执行记录表)。
我的思路
- 第一步 创建两种表用于存储定时任务信息以及执行记录信息。
- 第二步 需要记录每条执行的状态结果,执行中,执行失败,执行成功等,不可能在每个Job类中写,本来是想用aop切面,但是一直不生效。后来发现可以通过JobListener监听器的方式实现。这里需要注意就是,在执行开始新增记录,在执行结束或异常更新记录,为保证取得记录的logId,在执行开始时把logId放在ThreadLocal中,执行结束在从ThreadLocal取的logId。
- 第三步 JobListener监听器需要监听那些任务,需要在JobSchedule初始化的时候指定。第二步中的static {}静态代码块中
- 第四步 为了确保项目启动时,数据库中启动状态的项目正常启动,所以需要在项目初始化时启动数据库中的任务。
- 通过页面控制的话,我需要提供一些接口如:新增一个定时任务,修改执行计划/启动或关闭,查询某个任务的执行记录等。
- 对定时任务的操作状态需要同步更新到数据库中,所以我在service中调用JobSchedule工具类控制任务执行,同时跟新数据的任务状态等信息。
加载依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.3</version>
</dependency>
第一步:创建数据库
- schedule_job:用于记录每个定时任务的信息
CREATE TABLE `schedule_job` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(100) NOT NULL COMMENT '任务名称',
`clazz` varchar(100) NOT NULL COMMENT '任务执行的类',
`cron` varchar(50) NOT NULL COMMENT 'cron表达式',
`status` int(11) NOT NULL DEFAULT '2' COMMENT '1 开启 2 关闭 ',
`job_group_name` varchar(50) NOT NULL DEFAULT 'DEFAULT_JOB_GROUP',
`trigger_group_name` varchar(50) NOT NULL DEFAULT 'DEFAULT_TRIGGER_GROUP',
`des` varchar(200) DEFAULT NULL COMMENT '描述',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- schedule_log:用于记录定时任务的执行详情
CREATE TABLE `schedule_log` (
`id` varchar(32) NOT NULL COMMENT '主键',
`job_id` varchar(100) NOT NULL COMMENT '定时任务的id',
`job_name` varchar(100) DEFAULT NULL,
`job_clazz` varchar(100) DEFAULT NULL COMMENT '任务执行的类',
`status` int(11) NOT NULL DEFAULT '1' COMMENT '1 成功 2 异常 ',
`log_info` text COMMENT '描述',
`create_time` bigint(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第二步:创建一个定时任务的工具类
- JobSchedule 工具类提供对任务的 添加,修改,删除操作。
- 提供对定时任务的控制页面的话,就是通过Controller对应的service中调用此工具类,来实现对定时任务的控制。(后面会有Controller - service)的代码
public class JobSchedule {
private static final Logger LOGGER = LoggerFactory.getLogger(JobSchedule.class);
private static SchedulerFactory gSchedulerFactory = new StdSchedulerFactory();
private static String JOB_GROUP_NAME = "DEFAULT_JOB_GROUP";
private static String TRIGGER_GROUP_NAME = "DEFAULT_TRIGGER_GROUP";
static {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
sched.getListenerManager().addJobListener(new MyJobListener(),jobGroupEquals(JOB_GROUP_NAME));
}catch (Exception e){
LOGGER.error(">>>>>>>>>>>>>>>>>>>>>> 定时任务addJobListener失败 >>>>>>>>>>>>>>>>>>>>>>");
}
}
private JobSchedule() {
}
public static void addJob(String jobName, Class cls, String cron) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
JobDetail jobDetail = JobBuilder.newJob(cls).withIdentity(jobName, JOB_GROUP_NAME).build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, TRIGGER_GROUP_NAME)
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
sched.scheduleJob(jobDetail, trigger);
LOGGER.info("添加任务:{},{},{}",jobName,cls,cron);
if (!sched.isShutdown()) {
sched.start();
}
}
public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class cls, String cron) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
JobDetail jobDetail = JobBuilder.newJob(cls).withIdentity(jobName, jobGroupName).build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, triggerGroupName)
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
sched.scheduleJob(jobDetail, trigger);
LOGGER.info("添加任务:{},{},{},{},{},{}",jobName,jobGroupName,triggerName,triggerGroupName,cls,cron);
if (!sched.isShutdown()) {
sched.start();
}
}
public static void modifyJobTime(String jobName, String cron) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);
CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(cron)) {
JobDetail jobDetail = sched.getJobDetail(new JobKey(jobName, JOB_GROUP_NAME));
Class objJobClass = jobDetail.getJobClass();
removeJob(jobName);
addJob(jobName, objJobClass, cron);
LOGGER.info("修改任务:{},{}",jobName,cron);
}
}
public static void modifyOrAddJobTime(String jobName, String cron, Class cls) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);
CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
if (trigger == null) {
addJob(jobName, cls, cron);
LOGGER.info("修改任务:{},{}",jobName,cron);
}else{
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(cron)) {
JobDetail jobDetail = sched.getJobDetail(new JobKey(jobName, JOB_GROUP_NAME));
Class objJobClass = jobDetail.getJobClass();
removeJob(jobName);
addJob(jobName, objJobClass, cron);
LOGGER.info("修改任务:{},{}",jobName,cron);
}
}
}
public static void removeJob(String jobName) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
JobKey jobKey = new JobKey(jobName, TRIGGER_GROUP_NAME);
sched.pauseJob(jobKey);
sched.unscheduleJob(new TriggerKey(jobName, TRIGGER_GROUP_NAME));
sched.deleteJob(jobKey);
LOGGER.info("移除任务:{}",jobName);
}
public static void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
JobKey jobKey = new JobKey(jobName, jobGroupName);
sched.pauseJob(jobKey);
sched.unscheduleJob(new TriggerKey(jobName, triggerGroupName));
sched.deleteJob(jobKey);
LOGGER.info("移除任务:{},{},{},{},{},{}",jobName,jobGroupName,triggerName,triggerGroupName);
}
public static void startJobs() throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
sched.start();
LOGGER.info("启动所有任务");
}
public static void shutdownJobs() throws SchedulerException {
Scheduler sched = gSchedulerFactory.getScheduler();
if (!sched.isShutdown()) {
sched.shutdown();
LOGGER.info("关闭所有任务");
}
}
}
第三步:定时任务监听器MyJobListener
- 定时任务监听器:通过监听的方式记录 记录定时任务的执行记录。
- 创建日志的时候回把logId放在ThreadLocal中,在执行结果从ThreadLocal取logId来更新日志状态。
@Component
public class MyJobListener implements JobListener{
private JobService jobService;
ThreadLocal<String> threadLocal = new ThreadLocal<>();
@Override
public String getName() {
return "myJobListener";
}
@Override
public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
if(jobService == null){
jobService = SpringUtils.getBean(JobService.class);
}
JobDetail jobDetail = jobExecutionContext.getJobDetail();
String name = jobDetail.getKey().getName();
String logId = jobService.saveScheduleLog(name, JobLogStatusEnum.RUNNING.getCode(), "");
threadLocal.set(logId);
}
@Override
public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
String logId = threadLocal.get();
jobService.updateScheduleLog(logId, JobLogStatusEnum.ERROR.getCode(), "执行失败");
}
@Override
public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
String logId = threadLocal.get();
if(e == null){
jobService.updateScheduleLog(logId, JobLogStatusEnum.SUCCESS.getCode(), "执行成功");
}else{
jobService.updateScheduleLog(logId, JobLogStatusEnum.ERROR.getCode(), e.toString());
}
}
}
第四步:重启项目的时候确保定时任务启动
- 需要在项目初始化的时候,把数据中启动的任务添加到数据库中
- 该类会在项目启动后执行: xxx implements ServletContextAware
@Configuration
public class ScheduleJobInitConfig implements ServletContextAware {
private static final Logger logger = LoggerFactory.getLogger(ScheduleJobInitConfig.class);
@Autowired
private JobService jobService;
@Override
public void setServletContext(ServletContext servletContext) {
jobService.initScheduleJob();
logger.info("初始化定时任务成功 ...");
}
}
第五步:创建一个定时任务
@Component
public class JobA implements Job {
private static final Logger logger = LoggerFactory.getLogger(JobA.class);
private JobService jobService;
@Override
public void execute(JobExecutionContext context) {
System.out.println("=================>JobA ");
}
}
控制任务的接口方法,对任务的增删该查 都在 下面的service类中
- 完成以上这些,定时任务模块基本完成。接下来的就是为了控制界面提供的接口。我们这边前后的分离于是我直接使用postman调用接口控制。
- controller
@RestController
@RequestMapping("/v1/schedule/job")
public class JobController {
@Autowired
private JobService jobService;
@GetMapping("/list")
public R getJobList(){
try{
Map<String, Object> result = jobService.getAllJob();
return R.data(result);
}catch (Exception e){
return R.error(e);
}
}
@GetMapping("/schedule/detail")
public R getJobScheduleDetail(@RequestParam("jobId") int jobId){
try{
Map<String, Object> result = jobService.getJobScheduleDetail(jobId);
return R.data(result);
}catch (Exception e){
return R.error(e);
}
}
@GetMapping("/detail")
public R getJobDetail(@RequestParam("jobId") int jobId){
try{
Map<String, Object> result = jobService.getJobDetail(jobId);
return R.data(result);
}catch (Exception e){
return R.error(e);
}
}
@PostMapping("/update")
public R updateJob(ScheduleJob job){
try{
Map<String, Object> result = jobService.updateJob(job);
return R.data(result);
}catch (Exception e){
return R.error(e);
}
}
}
@Service
public class JobService {
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
@Autowired
private ScheduleJobRepoImpl scheduleJobRepo;
@Autowired
private ScheduleLogRepoImpl scheduleLogRepo;
public Map<String,Object> getAllJob(){
List<ScheduleJob> list = scheduleJobRepo.getList();
Map<String,Object> result = new HashMap<>();
result.put("list", list);
return result;
}
public Map<String,Object> getJobScheduleDetail(int jobId){
List<ScheduleLog> list = scheduleLogRepo.getListByJobId(jobId);
Map<String,Object> result = new HashMap<>();
result.put("list", list);
return result;
}
public Map<String,Object> getJobDetail(int jobId){
ScheduleJob scheduleJob = scheduleJobRepo.getScheduleJobById(jobId);
Map<String,Object> result = new HashMap<>();
result.put("scheduleJob", scheduleJob);
return result;
}
public Map<String,Object> updateJob(ScheduleJob scheduleJob){
Map<String,Object> result = new HashMap<>();
Class<?> clszz = null;
try {
clszz = Class.forName(scheduleJob.getClazz());
}catch (ClassNotFoundException e){
logger.error("修改定时任务异常,无法找到指定的任务类");
throw new AppException(ErrorCode.SYS_PARAMS_ERROR.code(), "无法找到指定的任务类");
}
try {
Integer status = scheduleJob.getStatus();
if(status == 1){
JobSchedule.modifyOrAddJobTime(scheduleJob.getName(), scheduleJob.getCron(),clszz);
}else if(status == 2){
JobSchedule.removeJob(scheduleJob.getName());
}else{
return result;
}
}catch (SchedulerException e){
logger.error("修改定时任务异常:{}", e);
throw new AppException(ErrorCode.SYS_ERROR);
}
scheduleJobRepo.updateScheduleJob(scheduleJob);
return result;
}
public void initScheduleJob(){
List<ScheduleJob> list = scheduleJobRepo.getEnableList();
for (ScheduleJob scheduleJob: list){
Class<?> clszz = null;
try {
clszz = Class.forName(scheduleJob.getClazz());
JobSchedule.addJob(scheduleJob.getName(), clszz, scheduleJob.getCron());
} catch (SchedulerException e){
logger.error("初始化启动定时任务异常:{}", e);
continue;
} catch (ClassNotFoundException e){
logger.error("初始化启动定时任务异常,无法找到指定的任务类");
continue;
}
}
}
public String saveScheduleLog(String name, int status, String logInfo){
ScheduleJob scheduleJob = scheduleJobRepo.getScheduleJobByName(name);
ScheduleLog scheduleLog = new ScheduleLog(scheduleJob,status,logInfo);
scheduleLogRepo.saveScheduleLog(scheduleLog);
return scheduleLog.getId();
}
public void updateScheduleLog(String id, int status, String logInfo){
scheduleLogRepo.updateScheduleLog(id,status,logInfo);
}
}
最后
- 如果在这里获得过启发和思考,希望点赞支持!对于内容有不同的看法欢迎来信交流。
- 技术栈 >> java
- 邮箱 >> 15673219519@163.com