Spring boot + quartz 实现动态管理任务

在实际项目应用中经常会用到定时任务,可以通过quartz和spring的简单配置即可完成,但如果要改变任务的执行时间、频率,废弃任务等就需要改变配置甚至代码需要重启服务器,这里介绍一下如何通过quartz与spring的组合实现动态的改变定时任务的状态的一个实现。

配置文件

相关jar包依赖配置

这里使用的 gradle 进行依赖管理。包含了 jpa, quartz, thymeleaf 和 web 等依赖

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-quartz')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile("org.springframework.boot:spring-boot-devtools")
    runtime('mysql:mysql-connector-java')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true  

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.joda-date-time-format=yyyy-MM-dd HH:mm:ss

其中 spring.jpa.open-in-view 和 spring.jpa.properties.hibernate.enable_lazy_load_no_trans 用于处理 hibernate 懒加载相关问题

quartz.properties

# thread-pool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=10
# job-store
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore

任务信息相关操作

数据库层代码

实体类映射数据库表

@Entity
@JsonIgnoreProperties(value={"hibernateLazyInitializer","handler","fieldHandler"}) 
public class QuartzJobBean implements Serializable {

    private static final long serialVersionUID = 607415834012939242L;

    public static final String STATUS_RUNNING = "1";
    public static final String STATUS_NOT_RUNNING = "0";
    public static final String STATUS_DELETED = "2";
    public static final String CONCURRENT_IS = "1";
    public static final String CONCURRENT_NOT = "0";

    /** 任务id */
    @Id
    @GeneratedValue
    private long jobId;

    /** 任务名称 */
    private String jobName;

    /** 任务分组,任务名称+组名称应该是唯一的 */
    private String jobGroup;

    /** 任务初始状态 0禁用 1启用 2删除 */
    private String jobStatus;

    /** 任务是否有状态(并发与否) */
    private String isConcurrent = "1";

    /** 任务运行时间表达式 */
    private String cronExpression;

    /** 任务描述 */
    private String description;

    /** 任务调用类在spring中注册的bean id,如果spingId不为空,则按springId查找 */
    private String springId;

    /** 任务调用类名,包名+类名,通过类反射调用 ,如果spingId为空,则按jobClass查找 */
    private String jobClass;

    /** 任务调用的方法名 */
    private String methodName;

    /** 启动时间 */
    private Date startTime;

    /** 前一次运行时间 */
    private Date previousTime;

    /** 下次运行时间 */
    private Date nextTime;

    public QuartzJobBean() {
        super();
    }

    public QuartzJobBean(String jobName, String jobGroup) {
        super();
        this.jobName = jobName;
        this.jobGroup = jobGroup;
    }

    public QuartzJobBean(String jobName, String jobGroup, String jobStatus, String isConcurrent,
            String cronExpression, String description, String springId, String jobClass, String methodName) {
        super();
        this.jobName = jobName;
        this.jobGroup = jobGroup;
        this.jobStatus = jobStatus;
        this.isConcurrent = isConcurrent;
        this.cronExpression = cronExpression;
        this.description = description;
        this.springId = springId;
        this.jobClass = jobClass;
        this.methodName = methodName;
    }

    // getter, setter

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(120);
        sb.append("QuartzJobBean [jobId=").append(jobId).append(", jobName=").append(jobName)
          .append(", jobGroup=").append(jobGroup).append(", jobStatus=").append(jobStatus)
          .append(", isConcurrent=").append(isConcurrent).append(", cronExpression=").append(cronExpression)
          .append(", jobClass=").append(jobClass).append(", methodName=").append(methodName).append("]");
        return sb.toString();
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + jobName.hashCode();
        hash = 31 * hash + jobGroup.hashCode();
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null || (obj.getClass() != this.getClass())) {
            return false;
        }
        QuartzJobBean oBean = (QuartzJobBean) obj;
        if(this.jobName.equals(oBean.jobName) && this.jobGroup.equals(oBean.jobGroup)) {
            return true;
        }
        return false;
    }

}

1) 这里使用了 JPA, 项目启动后 Hibernate 会自动在数据库创建/更新表结构
2) @JsonIgnoreProperties 注解用于在将对象转成json时忽略类中不存在的字段

继承JpaRepository类定义数据库操作接口

@Repository
public interface QuartzJobRepository extends JpaRepository<QuartzJobBean, Long> {

    List<QuartzJobBean> findByJobStatus(String jobStatus);

    List<QuartzJobBean> findByJobStatusNot(String jobStatus);

    // 修改上一次执行时间和下一次执行时间
    @Modifying
    @Query("update QuartzJobBean j set j.previousTime = ?1, j.nextTime = ?2 where j.jobId = ?3")
    int modifyByIdAndTime(Date previousTime, Date nextTime, Long jobId);

    // 修改job状态
    @Modifying
    @Query("update QuartzJobBean j set j.jobStatus = ?1 where j.jobId = ?2")
    int modifyByStatus(String jobStatus, Long jobId);

}

业务层处理

@Service("moduleService")
public class QuartzJobServiceImpl implements QuartzJobService {

    @Autowired
    private QuartzJobRepository repository;

    @Override
    public List<QuartzJobBean> findAll() {
        return repository.findAll();
    }

    @Transactional
    @Override
    public QuartzJobBean save(QuartzJobBean jobBean) {
        return repository.save(jobBean);
    }

    @Override
    public QuartzJobBean getOne(long jobId) {
        return repository.getOne(jobId);
    }

    @Transactional
    @Override
    public int modifyByIdAndTime(Date previousTime, Date nextTime, Long jobId) {
        return repository.modifyByIdAndTime(previousTime, nextTime, jobId);
    }

    @Override
    public List<QuartzJobBean> findByJobStatus(String jobStatus) {
        return repository.findByJobStatus(jobStatus);
    }

    @Override
    public List<QuartzJobBean> findByJobStatusNot(String jobStatus) {
        return repository.findByJobStatusNot(jobStatus);
    }

    @Transactional
    @Override
    public int modifyByStatus(String jobStatus, Long jobId) {
        return repository.modifyByStatus(jobStatus, jobId);
    }

}

job 相关类

定义 job 实现类

无状态的Job实现类

/**     
 * Job实现类  无状态     
 * 若此方法上一次还未执行完,而下一次执行时间轮到时则该方法也可并发执行     
 */    
public class QuartzJobFactory implements Job {

    private final Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail job = context.getJobDetail();
        JobKey key = job.getKey();
        String jobIdentity = "scheduleJob" + key.getGroup() + "_" + key.getName();
        Trigger trigger = context.getTrigger();
        QuartzJobBean scheduleJob = (QuartzJobBean) context.getMergedJobDataMap().get(jobIdentity);
        logger.info("运行任务名称 = [" + scheduleJob + "]");

        try {
            DTSResult result = TaskUtils.invokMethod(scheduleJob);

            scheduleJob.setNextTime(trigger.getNextFireTime());
            scheduleJob.setPreviousTime(trigger.getPreviousFireTime());

            QuartzJobService jobService = SpringUtils.getBean("moduleService");
            jobService.modifyByIdAndTime(scheduleJob.getPreviousTime(), scheduleJob.getNextTime(), scheduleJob.getJobId());

            // 写入运行结果
            DTSResultService dtsService = SpringUtils.getBean("dtsResultService");
            dtsService.save(result);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

}

注:
1) 当一个 job 触发时, execute 方法就会被调用,执行里面的操作。这里具体取出 MergedJobDataMap 中存放的 Job 实例,并通过调用 TaskUtils.invokMethod 方法来通过反射获取对象并执行对象的方法,记录执行结果并写入数据库。代码如下:

public class TaskUtils {

    private static Logger logger = LogManager.getLogger(TaskUtils.class);

    /**
     * 通过反射调用scheduleJob中定义的方法
     * 
     * @param scheduleJob
     */
    public static DTSResult invokMethod(QuartzJobBean scheduleJob) {
        Object object = null;
        Class<?> clazz = null;
        DTSResult result = new DTSResult();;
        long start = System.currentTimeMillis();

        try {
            // springId不为空先按springId查找bean
            String springId = scheduleJob.getSpringId();
            String beanClass = scheduleJob.getJobClass();
            if (springId != null && !"".equals(springId.trim())) {
                object = SpringUtils.getBean(springId);
            } else if (beanClass != null && !"".equals(beanClass.trim())) {
                try {
                    clazz = Class.forName(scheduleJob.getJobClass());
                    object = clazz.newInstance();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            if (object == null) {
                throw new Exception("任务名称 = [" + scheduleJob.getJobName() + "]---------------未启动成功,请检查是否配置正确!!!");
            }

            clazz = object.getClass();
            Method method = clazz.getDeclaredMethod(scheduleJob.getMethodName());
            if (method != null) {  
                method.invoke(object);

                result.setSuccess(true);
            }
        } catch (Exception e) {
            String errorMsg = e.getMessage();
            logger.error(errorMsg, e);

            result.setSuccess(true);
            result.setErrorMsg(errorMsg);
        }

        long end = System.currentTimeMillis();
        result.setDuration(String.valueOf((end - start)));
        result.setCreateTime(new Date());
        result.setJobId(scheduleJob.getJobId());
        return result;
    }

    /**     
     * 判断cron时间表达式正确性     
     * @param cronExpression     
     * @return      
     */     
    public static boolean isValidExpression(final String cronExpression) {
        CronTriggerImpl trigger = new CronTriggerImpl();
        try {
            trigger.setCronExpression(cronExpression);
            Date date = trigger.computeFirstFireTime(null);
            return date !=null && date.after(new Date());
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return false;
    }

    /*     
     * 任务运行状态     
     */     
    public enum TASK_STATE{     
        NONE("NONE","未知"),     
        NORMAL("NORMAL", "正常运行"),     
        PAUSED("PAUSED", "暂停状态"),      
        COMPLETE("COMPLETE",""),     
        ERROR("ERROR","错误状态"),     
        BLOCKED("BLOCKED","锁定状态"); 

        private String index;       
        private String name;       

        private TASK_STATE(String index, String name) {
            this.name = name;        
            this.index = index; 
        }

        public String getIndex() {
            return index;
        }

        public String getName() {
            return name;
        }
    }

}

2) 这里无法使用 @Autowired 注入 Service,于是定义了 SpringUtils 工具类用于访问 spring 中相关bean。相关代码如下:

// 实现 ApplicationContextAware 接口,用于获取 ApplicationContext 对象,从而获取 spring 管理的bean
@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String beanName) {
        if(applicationContext.containsBean(beanName)) {
            return (T) applicationContext.getBean(beanName);
        } else {
            return null;
        }
    }

    public static <T> Map<String, T> getBeansOfType(Class<T> baseType){
        return applicationContext.getBeansOfType(baseType);
    }

    public static boolean isNotBlank(String str) {
        boolean result = true;
        if(null == str || "".equals(str.trim())) {
            result = false;
        }
        return result;
    }

}

有状态的 job 实现类

package com.johnfnash.learn.jobs;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.johnfnash.learn.domain.DTSResult;
import com.johnfnash.learn.domain.QuartzJobBean;
import com.johnfnash.learn.service.DTSResultService;
import com.johnfnash.learn.service.QuartzJobService;
import com.johnfnash.learn.util.SpringUtils;
import com.johnfnash.learn.util.TaskUtils;

/**     
 *  Job有状态实现类,不允许并发执行
 *  若一个方法一次执行不完下次轮转时则等待该方法执行完后才执行下一次操作     
 *  主要是通过注解:@DisallowConcurrentExecution (quartz 2.3.0 版本中 StatefulJob 已经被弃用,改用注解的方式)
 */   
@DisallowConcurrentExecution     
public class QuartzJobFactoryDisallowConcurrentExecution implements Job {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 内容与 QuartzJobFactory 里的 execute 方法里的一致
    }

}

注:

job 管理

先定义 SchedulerFactoryBean bean,用于获取 Scheulder

@Configuration
public class SchedulerConfig {

    @Bean(name = "schedulerFactoryBean")
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();     
        return schedulerFactoryBean;
    }

}

本次的关键,job 的新增、更改、删除、暂停、恢复等操作

@Service
public class TaskService {

    @SuppressWarnings("unused")
    private final Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @Autowired     
    private SchedulerFactoryBean schedulerFactoryBean;   

    @Autowired
    private QuartzJobRepository repository;

    /**    
     * 获取单个任务    
     * @param jobName    
     * @param jobGroup    
     * @return    
     * @throws SchedulerException    
     */    
    public QuartzJobBean getJob(String jobName,String jobGroup) throws SchedulerException {
        QuartzJobBean job = null;    
        Scheduler scheduler = getScheduler();
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);    
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        if (null != trigger) {
            job = createJob(jobName, jobGroup, scheduler, trigger);
        }

        return job;
    }

    private Scheduler getScheduler() {
        return schedulerFactoryBean.getScheduler();
    }

    private QuartzJobBean createJob(String jobName, String jobGroup, Scheduler scheduler, Trigger trigger)
            throws SchedulerException {
        QuartzJobBean job;
        job = new QuartzJobBean();
        job.setJobName(jobName);    
        job.setJobGroup(jobGroup);    
        job.setDescription("触发器:" + trigger.getKey()); 
        job.setNextTime(trigger.getNextFireTime());
        job.setPreviousTime(trigger.getPreviousFireTime());

        Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
        job.setJobStatus(triggerState.name());

        if(trigger instanceof CronTrigger) {
            CronTrigger cronTrigger = (CronTrigger)trigger;
            String cronExpression = cronTrigger.getCronExpression();
            job.setCronExpression(cronExpression);
        }
        return job;
    }

    /**    
     * 获取所有任务    
     * @return    
     * @throws SchedulerException    
     */    
    public List<QuartzJobBean> getAllJobs() throws SchedulerException{   
        Scheduler scheduler = getScheduler();
        GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
        Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
        List<QuartzJobBean> jobList = new ArrayList<QuartzJobBean>();
        List<? extends Trigger> triggers;
        QuartzJobBean job;
        for (JobKey jobKey : jobKeys) {
            triggers = scheduler.getTriggersOfJob(jobKey);
            for (Trigger trigger : triggers) {
                job = createJob(jobKey.getName(), jobKey.getGroup(), scheduler, trigger);
                jobList.add(job);
            }
        }

        return jobList;
    }

    /**    
     * 所有正在运行的job    
     *     
     * @return    
     * @throws SchedulerException    
     */    
    public List<QuartzJobBean> getRunningJob() throws SchedulerException {
        Scheduler scheduler = getScheduler();
        List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
        List<QuartzJobBean> jobList = new ArrayList<QuartzJobBean>(executingJobs.size());
        QuartzJobBean job;
        JobDetail jobDetail;
        JobKey jobKey;

        for (JobExecutionContext executingJob : executingJobs) {
            jobDetail = executingJob.getJobDetail();
            jobKey = jobDetail.getKey();

            job = createJob(jobKey.getName(), jobKey.getGroup(), scheduler, executingJob.getTrigger());
            jobList.add(job);
        }

        return jobList;
    }

    /**    
     * 添加任务    
     *     
     * @param scheduleJob    
     * @throws SchedulerException    
     */    
    public boolean addJob(QuartzJobBean job) throws SchedulerException { 
        if(job == null || !QuartzJobBean.STATUS_RUNNING.equals(job.getJobStatus())) {
            return false;
        }

        String jobName = job.getJobName();
        String jobGroup = job.getJobGroup();
        if(!TaskUtils.isValidExpression(job.getCronExpression())) {
            logger.error("时间表达式错误("+jobName+","+jobGroup+"), "+job.getCronExpression());    
            return false;
        } else {
            Scheduler scheduler = getScheduler();
            // 任务名称和任务组设置规则:    // 名称:task_1 ..    // 组 :group_1 ..
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName,  jobGroup);
            Trigger trigger = scheduler.getTrigger(triggerKey);
            // 不存在,创建一个       
            if (null == trigger) { 
                // 表达式调度构建器
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
                // 按新的表达式构建一个新的trigger
                trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey)
                                                     .startAt(job.getStartTime()==null ? (new Date()) : job.getStartTime()) // 设置job不早于这个时间进行运行,和调用trigger的setStartTime方法效果一致
                                                     .withSchedule(scheduleBuilder).build();

                //是否允许并发执行
                JobDetail jobDetail = getJobDetail(job);
                // 将 job 信息存入数据库
                job.setStartTime(trigger.getStartTime());
                job.setNextTime(trigger.getNextFireTime());
                job.setPreviousTime(trigger.getPreviousFireTime());
                job = repository.save(job);
                jobDetail.getJobDataMap().put(getJobIdentity(job), job);

                scheduler.scheduleJob(jobDetail, trigger);

            } else { // trigger已存在,则更新相应的定时设置  
                // 更新 job 信息到数据库
                job.setStartTime(trigger.getStartTime());
                job.setNextTime(trigger.getNextFireTime());
                job.setPreviousTime(trigger.getPreviousFireTime());
                job = repository.save(job);
                getJobDetail(job).getJobDataMap().put(getJobIdentity(job), job);

                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
                // 按新的表达式构建一个新的trigger
                trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey)
                                                     .startAt(job.getStartTime()==null ? (new Date()) : job.getStartTime()) // 设置job不早于这个时间进行运行,和调用trigger的setStartTime方法效果一致
                                                     .withSchedule(scheduleBuilder).build();
                scheduler.rescheduleJob(triggerKey, trigger);
            }
        }
        return true;
    }

    private String getJobIdentity(QuartzJobBean job) {
        return "scheduleJob"+(job.getJobGroup() +"_"+job.getJobName());
    }

    private JobDetail getJobDetail(QuartzJobBean job) {
        Class<? extends Job> clazz = QuartzJobBean.CONCURRENT_IS.equals(job.isConcurrent()) 
                        ? QuartzJobFactory.class : QuartzJobFactoryDisallowConcurrentExecution.class;       
        JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(job.getJobName(), job.getJobGroup()).build();
        return jobDetail;
    }

    /**    
     * 暂停任务    
     * @param job    
     * @return    
     */ 
    @Transactional
    public boolean pauseJob(QuartzJobBean job){    
        Scheduler scheduler = getScheduler();
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        boolean result;
        try {
            scheduler.pauseJob(jobKey);

            // 更新任务状态到数据库
            job.setJobStatus(QuartzJobBean.STATUS_NOT_RUNNING);
            repository.modifyByStatus(job.getJobStatus(), job.getJobId());

            result = true;
        } catch (SchedulerException e) {
            result = false;
            e.printStackTrace();
        }
        return result;
    }

    /**    
     * 恢复任务    
     * @param job    
     * @return    
     */    
    @Transactional
    public boolean resumeJob(QuartzJobBean job){
        Scheduler scheduler = getScheduler();
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        boolean result;
        try {
            logger.info("resume job : " + (job.getJobGroup() + "_" + job.getJobName()));
            TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
            // 表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
            Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey)
                                            .startAt(job.getStartTime()==null ? (new Date()) : job.getStartTime()) // 设置job不早于这个时间进行运行,和调用trigger的setStartTime方法效果一致
                                            .withSchedule(scheduleBuilder).build();
            scheduler.rescheduleJob(triggerKey, trigger);
            scheduler.resumeJob(jobKey);

            // 更新任务状态到数据库
            job.setJobStatus(QuartzJobBean.STATUS_RUNNING);
            repository.modifyByStatus(job.getJobStatus(), job.getJobId());

            result = true;
        } catch (SchedulerException e) {
            result = false;
            e.printStackTrace();
        }
        return result;
    }

    /**    
     * 删除任务    
     */    
    @Transactional
    public boolean deleteJob(QuartzJobBean job){ 
        Scheduler scheduler = getScheduler();    
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());    
        boolean result;
        try{    
            scheduler.deleteJob(jobKey);

            // 更新任务状态到数据库
            job.setJobStatus(QuartzJobBean.STATUS_DELETED);
            repository.modifyByStatus(job.getJobStatus(), job.getJobId());

            result = true;    
        } catch (SchedulerException e) {    
            result = false;
            e.printStackTrace();
        }    
        return result;    
    } 

    /**    
     * 立即执行一个任务    
     * @param scheduleJob    
     * @throws SchedulerException    
     */    
    public void startJob(QuartzJobBean scheduleJob) throws SchedulerException{
        Scheduler scheduler = getScheduler();  
        JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
        scheduler.triggerJob(jobKey);
    }

    /**    
     * 更新任务时间表达式    
     * @param job    
     * @throws SchedulerException    
     */    
    @Transactional
    public void updateCronExpression(QuartzJobBean job) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
        //获取trigger,即在spring配置文件中定义的 bean id="myTrigger"
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        //表达式调度构建器    
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        //按新的cronExpression表达式重新构建trigger
        trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
        //按新的trigger重新设置job执行
        scheduler.rescheduleJob(triggerKey, trigger);

        // 更新 job 信息到数据库
        job.setStartTime(trigger.getStartTime());
        job.setNextTime(trigger.getNextFireTime());
        job.setPreviousTime(trigger.getPreviousFireTime());
        job = repository.save(job);
        getJobDetail(job).getJobDataMap().put(getJobIdentity(job), job);
    }

    /**
     * 设置job的开始schedule时间
     * @param job
     * @throws SchedulerException
     */
    @Transactional
    public void updateStartTime(QuartzJobBean job) throws SchedulerException {
        Scheduler scheduler = getScheduler();
        TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
        //获取trigger,即在spring配置文件中定义的 bean id="myTrigger"
        CronTriggerImpl trigger = (CronTriggerImpl) scheduler.getTrigger(triggerKey);
        trigger.setStartTime(job.getStartTime());
        //按新的trigger重新设置job执行
        scheduler.rescheduleJob(triggerKey, trigger);

        // 更新 job 信息到数据库
        job.setStartTime(trigger.getStartTime());
        job.setNextTime(trigger.getNextFireTime());
        job.setPreviousTime(trigger.getPreviousFireTime());
        job = repository.save(job);
        getJobDetail(job).getJobDataMap().put(getJobIdentity(job), job);
    }

}

注:
1) Quartz 中 Job 的执行状态获取
使用Quartz定时调度Job,经常需要实时监控Job的执行状态。在这里,Quartz提供了getTriggerState方法来获取当前执行状态。
其中返回值分别代表意思如下:
**STATE_BLOCKED 4 阻塞
STATE_COMPLETE 2 完成
STATE_ERROR 3 错误
STATE_NONE -1 不存在
STATE_NORMAL 0 正常
STATE_PAUSED 1 暂停**
具体代码如下:

StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
int state = scheduler.getTriggerState(triggerName, triggerGroup);

服务启动时,读取数据库 job 信息,并进行 schedule

@Component
public class MyRunner implements CommandLineRunner {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired     
    private TaskService taskService;

    @Autowired
    private QuartzJobService jobService;

    @Override
    public void run(String... args) throws Exception {
        // 可执行的任务列表        
        List<QuartzJobBean> taskList = jobService.findByJobStatus(QuartzJobBean.STATUS_RUNNING);     
        logger.info("初始化加载定时任务......");     
        for (QuartzJobBean job : taskList) {     
            try {
                taskService.addJob(job);     
            } catch (Exception e) {
                logger.error("add job error: " + job.getJobName() + " " + job.getJobGroup(), e);
            }
        }  
    }

}

到此为止,job 可以随着服务一起启动了。
定义两个具体的任务,并配置数据

public class JobTest {

    public void run() {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println(df.format(new Date()) + " job executing...");
    }

}
public class JobViewTest {

    private TaskService taskService;

    public JobViewTest() {
        taskService = SpringUtils.getBean("taskService");
    }

    public void run() {
        List<QuartzJobBean> jobs;
        try {
            System.out.print("All jobs: ");
            jobs = taskService.getAllJobs();
            for (QuartzJobBean job : jobs) {
                System.out.print(job.getJobGroup() + "_" + job.getJobName() + " " + job.getJobStatus() + "\t");
            }
            System.out.println();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}

数据库中执行如下脚本:

INSERT INTO `quartz_job_bean` 
(`job_id`,`cron_expression`,`description`,`is_concurrent`,`job_class`,`job_group`,`job_name`,`job_status`,`method_name`,`next_time`,`previous_time`,`spring_id`,`start_time`) 
VALUES 
 ('1','*/20 * * * * ?','触发器task_1','1','com.johnfnash.learn.jobs.JobTest','group_1','task_1','1','run','2018-06-02 09:45:40','2018-06-02 09:45:20',NULL,'2018-05-29 09:02:20')
,('18','*/5 * * * * ?','task 5\t','1','com.johnfnash.learn.jobs.JobTest','group_1','task_5','0','run','2018-05-29 14:47:00',NULL,NULL,'2018-05-29 14:46:56')
,('2','0 * 14-19 * * ?','触发器task_2','1','com.johnfnash.learn.jobs.JobTest','group_1','task_2','0','run','2018-05-28 14:00:00','2018-05-28 14:00:00',NULL,'2018-05-28 10:25:48')
,('3','0 0/1 * * * ?','触发器task_3','1','com.johnfnash.learn.jobs.JobTest','group_1','task_3','0','run','2018-05-23 00:00:00','2018-05-22 00:00:00',NULL,'2018-05-23 00:00:00')
,('4','0 0/1 * * * ?','触发器task_4','1','com.johnfnash.learn.jobs.JobViewTest','group_1','task_4','0','run','2018-05-24 21:55:00','2018-05-24 21:54:00',NULL,'2018-05-24 21:54:00');

这样,当服务启动之后,定时服务就会随之一起启动了

注:可以添加一个定时任务,用于定期检查数据库中新增的定时任务,并进行schedule。这里就不实现了。

添加定时任务界面管理

controller

@Controller
@RequestMapping("/job")
public class QuartzJobController {

    @Autowired
    private QuartzJobService jobService;

    @Autowired
    private TaskService taskService;

    @GetMapping("/list")
    public String getJobList(Model model) {
        List<QuartzJobBean> jobList = jobService.findByJobStatusNot(QuartzJobBean.STATUS_DELETED);
        model.addAttribute("jobs", jobList);

        return "jobList";
    }

    @PutMapping("/{id}/status")
    @ResponseBody
    public List<QuartzJobBean> updateJobStatus(@PathVariable("id") Long id, @RequestParam("jobStatus") String jobStatus, Model model) {
        jobService.modifyByStatus(jobStatus, id);
        QuartzJobBean jobBean = jobService.getOne(id);

        List<QuartzJobBean> jobs;
        try {
            jobs = taskService.getAllJobs();
            if(QuartzJobBean.STATUS_RUNNING.equals(jobBean.getJobStatus()) && !jobs.contains(jobBean)) {
                taskService.addJob(jobBean);
            } else if(QuartzJobBean.STATUS_RUNNING.equals(jobBean.getJobStatus())  && jobs.contains(jobBean)) {
                taskService.resumeJob(jobBean);
            }

            if(QuartzJobBean.STATUS_NOT_RUNNING.equals(jobBean.getJobStatus()) && jobs.contains(jobBean)) {
                taskService.pauseJob(jobBean);
            } else if(QuartzJobBean.STATUS_NOT_RUNNING.equals(jobBean.getJobStatus()) && !jobs.contains(jobBean)) {
                jobService.modifyByStatus(jobBean.getJobStatus(), jobBean.getJobId());
            }
        } catch (SchedulerException e1) {
            e1.printStackTrace();
        }

        List<QuartzJobBean> jobList = jobService.findByJobStatusNot(QuartzJobBean.STATUS_DELETED);
        return jobList;
    }

    @PutMapping("/{id}/date")
    @ResponseBody
    public Map<String, String> updateNextRunDate(@PathVariable("id") Long id, @RequestParam("date") String date, Model model) {
        QuartzJobBean jobBean = jobService.getOne(id);
        if(jobBean != null && date != null) {
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date startTime;
            try {
                startTime = df.parse(date);
                jobBean.setStartTime(startTime);
                jobBean.setNextTime(startTime);
            } catch (ParseException e1) {
                e1.printStackTrace();
            }

            jobService.save(jobBean);
        }

        Map<String, String> result = new HashMap<String, String>();
        result.put("status", "success");
        return result;
    }

}

注:当使用 PUT 方式用于修改相关请求时,将参数放在data中会接收不到,最后将参数放在请求参数中(?后面),并配置了filter,最终可以接收到参数了。

import org.springframework.stereotype.Component;
import org.springframework.web.filter.HttpPutFormContentFilter;

@Component
public class PutFilter extends HttpPutFormContentFilter {

}

方法来自: springBoot PUT请求接收不了参数的解决办法

前台代码:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Task Management</title>
    <link rel="stylesheet" type="text/css" th:href="@{/style.css}"></link>
</head>
<body>
    <h2>Task Management</h2>
    <div th:unless="${#lists.isEmpty(jobs)}">
        <table>
            <thead>
                <tr><td colspan="5">Task List</td></tr>
                <tr>
                    <td>Task Name</td>
                    <td>State</td>
                    <td>Schedule</td>
                    <td>Next Run Date</td>
                    <td>Last Run Date</td>
                </tr>
            </thead>
            <tbody>             
                <tr th:each="job : ${jobs}">
                    <td th:text="${job.getJobName()}">Task Name</td>
                    <td th:switch="${job.getJobStatus()}">
                        <a class="stateHref" href="javascript:void(0)" th:case="1" th:rel="${job.jobId}">Enabled</a>
                        <a class="stateHref" href="javascript:void(0)" th:case="0" th:rel="${job.jobId}">Disabled</a>
                    </td>
                    <td>Schedule</td>
                    <td><a class="nextTimeHref" href="javascript:void(0)" th:text="${job.getNextTime()}" th:rel="${job.jobId}">Next Run Date</a></td>
                    <td th:text="${job.getPreviousTime()}">Last Run Date</td>
                </tr>
            </tbody>
        </table>
    </div>
    <div th:if="${#lists.isEmpty(jobs)}">
        <p>There is no job for now</p>
    </div>

    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script type="text/javascript">
        $(function() {
            $("body").on("click", ".stateHref", function() {
                var jobId = $(this).attr("rel");
                $(this).text($(this).text() == 'Enabled' ? 'Disabled' : 'Enabled');
                var jobStatus = $(this).text() == 'Enabled' ? '1' : '0';
                $.ajax(  
                    {
                        type : 'PUT',    
                        contentType : 'application/json',    
                        url : '/job/' + jobId + "/status?jobStatus=" + jobStatus,    
                        processData : false,    
                        dataType : 'json',    
                        success : function(result)   
                        {    
                            $("tbody tr").remove();

                            buildTable(result);
                        },    
                        error : function()   
                        {    
                            console.log('Err...');    
                        }    
                });    
            });

            $("body").on("click", ".nextTimeHref", function() {
                var jobId = $(this).parent().find("a").attr('rel');
                $(this).parent().append($('<input type="text" value="' + $(this).text() + '" ><br/><a href="javascript:void(0)" class="updateLink" rel="'
                            + jobId + '">Apply</a>'));
                $(this).hide();
            });

            $("body").on("click", ".updateLink", function() {
                var jobId = $(this).attr("rel");
                var date = $(this).parent().find("input[type=text]").val();
                var $self = $(this);
                var $a = $self.parent().find("a[class=nextTimeHref]");
                $a.text(date);

                $.ajax(  
                    {
                        type : 'PUT',    
                        contentType : 'application/json',    
                        url : '/job/' + jobId + "/date?date=" + date,    
                        processData : false,    
                        dataType : 'json',       
                        success : function(result)   
                        {    
                            $self.parent().find("input").remove();
                            $a.show();
                            $self.remove();                            
                        },    
                        error : function()   
                        {    
                            console.log('Err...');    
                        }    
                });    
            });
        });

        function buildTable(data) {
            if(Array.isArray(data) && data.length>0) {
                var $tbody = $('tbody');
                var template;
                $.each(data, function(idx, obj) {
                    template = '<tr><td>' + obj.jobName + '</td>'
                        + '<td><a class="stateHref" href="javascript:void(0)" rel="' + obj.jobId + '">' + (obj.jobStatus=='1' ? 'Enabled' : 'Disabled') + '</a></td>'
                        + '<td>Schedule</td>'
                        + '<td><a class="nextTimeHref" href="javascript:void(0)" rel="' + obj.jobId + '">' + obj.nextTime + '</a></td>'
                        + '<td>' + obj.previousTime + '</td>'
                        + '</tr>';
                    $tbody.append(template);
                });
            }
        }
    </script>
</body>
</html>

服务启动之后,访问 http://localhost:8080/job/list ,就可以看到 job 列表,并进行相关操作了。
注:这里界面上没有添加修改cron表达式的功能,可以自行添加(TaskService里已经有修改cron表达式的方法)

到此为止,quartz 任务动态管理功能就实现了。
完整代码

本文参考

1) Spring+quartz 实现动态管理任务

2) quartz spring 实现动态定时任务

3) 获取Quartz中Job的执行状态

4) Quartz 有状态的JobDataMap

猜你喜欢

转载自blog.csdn.net/xxc1605629895/article/details/80546521