Spring-job(quartz)任务监控界面(组件)

俺的第一个文章,有掌声的给掌声,没掌声的给鲜花啦!

起因:因系统的一个定时任务突然执行不正常了,原来是一个时跑一次,现在偶尔跑,偶尔不跑,日志跟踪二天只跑了一次,这个时间段内没有对系统做任务变更,日志也没有任务异常,用VisualVM远程JMX的方式不能正常监控到进程(待努力重试),因此临时起意想做一下任务监控界面,且形成一个组件,方便管理员查看所有任务列表,及方便调整,暂停等。

本来参考了网上一些例子,都不适合我的需求,因此自己写了一份。代码主要参考了quartz,spring-job相关官方代码及例子。

本文提供一种思路,也许你有更好实现,能否回复一下?一起讨论?

目标:对管理员来说,希望可看到每个任务信息,以及当前状态,历史执行情况及日志,可对当前任务可以暂停,启动,立即执行,查看异常。

当然,以下数据都是持久在数据库。

当然,我的考虑中,是将所有的任务都变成Corn Expression,也就是说使用CornTrigger,SimpleTrigger没被使用,没这个使用的方便。

大致效果如下:

 

 

我们需要通过界面来增加要管理的任务:

 

进一步考虑:

也许现在我们只要配置一个Spring BEAN即可,也许将来,有人写了直接继承org.quartz.Job或者QuartzJobBean也要能支持:

  1: public class DemoJob extends AbstractJob {
  2: 
  3:   @Override
  4:   public void execute(JobDataMap jobDataMap) throws Exception {
  5:     logger.debug("DEMO JOB开始运行:"+jobDataMap.getWrappedMap());
  6: 
  7:   }
  8: 
  9: }

也许有人写了个类,只想执行里面的一个方法也可以执行,如

 

  1: public class NonQuartzJob {
  2:   public void execute() {
  3:     System.out.println("NonQuartzJob Runned:"+jobEntityService);
  4:     try {
  5:       Thread.sleep(8000);
  6:     } catch (InterruptedException e) {
  7:       // TODO Auto-generated catch block
  8:       e.printStackTrace();
  9:     }
 10:     throw new RuntimeException("test");
 11:   }
 12: }

现状:现在的Job都是独立实现,然后用spring配置式实现,都是采用如下方式配置

  1: <!-- 原始任务 -->
  2:   <bean id="queryStatementState" class="com.apusic.nsec.settlement.job.impl.QueryStatementState">
  3:     <property name="settlementService" ref="settlementService"></property>
  4:   </bean>
  5: <!-- 包装成Spring任务 -->
  6:   <bean name="checkDiskJob"
  7:     class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  8:     <property name="targetObject" ref="queryStatementState" />
  9:     <property name="targetMethod" value="queryStatement" />
 10:     <property name="concurrent" value="false" />
 11:   </bean>
 12: 
 13:   <!-- Trigger-->
 14:   <bean id="repeatTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
 15:     <property name="jobDetail" ref="checkDiskJob" />
 16:     <!-- 5分钟后启动-->
 17:     <property name="startDelay" value="300000" />
 18:     <!--  30分钟检查一次-->
 19:     <property name="repeatInterval" value="1800000" />
 20:   </bean>
 21: 
 22:   <!-- 调度器-->
 23:   <bean id="scheduler"
 24:     class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 25:     <property name="triggers">
 26:       <list>
 27:         <ref bean="repeatTrigger" />
 28:       </list>
 29:     </property>
 30:   </bean>

实现:

1.实体定义:

  1: @Entity
  2: public class JobEntity extends IdEntity {
  3:   @NotBlank
  4:   @Column(unique = true)
  5:   private String jobName;// 任务名
  6:   @NotBlank
  7:   private String jobClass;// 类名或者bean名
  8:   private String jobMethod;// 如果为bean名,对应执行的方法
  9:   @NotNull
 10:   private String jobCronExpress;// 表达式
 11:   private String jobDesc;// 任务描述
 12:   private String jobGroupName;// Group名
 13:   @ElementCollection(fetch = FetchType.LAZY)
 14:   @CollectionTable(name = "t_job_properties")
 15:   private Map<String, String> properties = new HashMap<String, String>();
 16:   private int jobExecCount;
 17:   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 18:   @Temporal(TemporalType.TIMESTAMP)
 19:   private Date createTime = new Date();
 20:   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 21:   @Temporal(TemporalType.TIMESTAMP)
 22:   private Date lastExecTime;
 23:   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 24:   @Temporal(TemporalType.TIMESTAMP)
 25:   private Date nextExecTime;
 26:   // true=继承Job类,false=spring bean,没有继承job类
 27:   private boolean jobClassIsBeanName = false;
 28:   @Enumerated(EnumType.STRING)
 29:   private JobStatus status = JobStatus.WAITTING;
 30: 
 31:   private long jobUsedTime;// ms
 32:   private long jobExceptionCount;//任务异常总数
 33:   
 34:   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 35:   @Temporal(TemporalType.TIMESTAMP)
 36:   private Date lastExeceptionTime;

日志记录

  1: @Entity
  2: public class JobLogEntity extends IdEntity {
  3:   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  4:   @Temporal(TemporalType.TIMESTAMP)
  5:   private Date execTime = new Date();
  6:   @Enumerated(EnumType.STRING)
  7:   private JobLogStatus status = JobLogStatus.SUCCESS;
  8: 
  9:   @ManyToOne
 10:   private JobEntity jobEntity = new JobEntity();
 11:   @Column(length = 4000)
 12:   private String exceptionStackTrace;

 

2.接口定义

  1: 
  2: public interface QuartzFacade {
  3: 
  4:   public void startJobs(List<JobEntity> jobEntitys) throws SchedulerException, ClassNotFoundException,
  5:       NoSuchMethodException;
  6: 
  7:   public void startJob(JobEntity jobEntity) throws SchedulerException, ClassNotFoundException, NoSuchMethodException;
  8: 
  9:   public void startJobImmediatelyOnce(JobEntity jobEntity) throws SchedulerException, ClassNotFoundException,
 10:       NoSuchMethodException;
 11: 
 12:   public void startScheduler() throws SchedulerException;
 13: 
 14:   public void pauseJob(JobEntity jobEntity) throws SchedulerException;
 15: 
 16:   public void resumeJob(JobEntity jobEntity) throws SchedulerException;
 17: 
 18:   public void pauseAll() throws SchedulerException;
 19: 
 20:   public void shutdownAll() throws SchedulerException;
 21: 
 22:   public void saveJobEntity(JobEntity jobEntity);
 23: 
 24:   public void updateJobEntity(JobEntity jobEntity);
 25: 
 26:   public void removeJobEntity(JobEntity jobEntity);
 27: 
 28:   public void deleteById(Long id);
 29: 
 30:   public JobEntity getById(Long id);
 31: 
 32:   public JobEntity findJobEntityByJobName(String jobName);
 33: 
 34:   public List<JobEntity> getAllJobEntitys();
 35: 
 36:   public Page<JobEntity> getAllJobEntitysAsPage(Page<JobEntity> p);
 37: 
 38:   public boolean isExistJobEntity(String jobName);
 39: …log相关
 40: }

3.实现

为了方便记录日志,或者增加操作,如果用切面,没有通用性,还不如定义父类。

3.1抽象JOB定义

在此我们包装了JOB,之后的JOB都必须继承此类,之前的已定义的须做相应的转换:

  1: public abstract class AbstractJob extends QuartzJobBean {
  2:   protected final static Log logger = LogFactory.getLog(AbstractJob.class);
  3: 
  4:   @Override
  5:   protected final void executeInternal(JobExecutionContext context) throws JobExecutionException {
  6:     JobDetail jobDetail = context.getJobDetail();
  7: 
  8:     JobEntity jobEntity = getJobEntityService().findJobEntityByJobName(jobDetail.getKey().getName());
  9:     JobLogEntity logEntity = preExecute(context, jobEntity);
 10: 
 11:     try {
 12:       long start = System.currentTimeMillis();
 13: 
 14:       execute(jobDetail.getJobDataMap());//
 15: 
 16:       jobEntity.setJobUsedTime(System.currentTimeMillis() - start);
 17:       jobEntity.setStatus(JobStatus.WAITTING);
 18:       getJobEntityService().updateJobEntity(jobEntity);
 19:       getJobLogEntityService().addJobLog(logEntity);
 20:     } catch (Exception e) {
 21:       logger.error("任务执行出错" + e.getMessage(), e);
 22:       dealException(jobEntity, logEntity, e);
 23:       throw new JobExecutionException(e);
 24:     }
 25: 
 26:   }
 27: 
 28:   private JobLogEntity preExecute(JobExecutionContext context, JobEntity jobEntity) throws JobExecutionException {
 29:     if (jobEntity == null) {
 30:       logger.error("您要执行的任务不存在:" + context.getJobDetail().getName());
 31:       throw new JobExecutionException("任务不存在:" + context.getJobDetail().getName());
 32:     }
 33: 
 34:     jobEntity.setStatus(JobStatus.RUNNING);
 35:     jobEntity.setLastExecTime(new Date());
 36:     jobEntity.setNextExecTime(context.getNextFireTime());
 37:     jobEntity.setJobExecCount(jobEntity.getJobExecCount() + 1);
 38: 
 39:     JobLogEntity logEntity = new JobLogEntity();
 40:     logEntity.setJobEntity(jobEntity);
 41:     getJobEntityService().updateJobEntity(jobEntity);
 42:     return logEntity;
 43:   }
 44: 
 45:   private void dealException(JobEntity jobEntity, JobLogEntity logEntity, Exception e) throws JobExecutionException {
 46:     ExceptionEventDispather.getInstance().notify(jobEntity);
 47:     logEntity.setStatus(JobLogStatus.FAIL);
 48:     logEntity.setExceptionStackTrace(Util.getStackTrack(e));
 49:     try {
 50:       getJobLogEntityService().addJobLog(logEntity);
 51:       jobEntity.setJobExceptionCount(jobEntity.getJobExceptionCount() + 1);
 52:       jobEntity.setStatus(JobStatus.EXCEPTION);
 53:       jobEntity.setLastExeceptionTime(new Date());
 54:       getJobEntityService().updateJobEntity(jobEntity);
 55:     } catch (Exception e1) {
 56:       throw new JobExecutionException(e1);
 57:     }
 58:   }
 59: 
 60:   @Transactional
 61:   public abstract void execute(JobDataMap jobDataMap) throws Exception;

同时定义了事件派发,以便任务出错,或者出错多少次时,发送邮件到管理员的邮件.

Job代码:

  1: public class DemoSpringJob  extends AbstractJob {
  2:   @Autowired
  3:   JobEntityService jobEntityService;
  4:   @Override
  5:   public void execute(JobDataMap jobDataMap) throws Exception {
  6:     //JobEntity job = jobEntityService.findJobEntityByJobName("Demo任务_1325661555923");
  7:     logger.debug("DemoSpringJob Runned:"+jobEntityService);
  8:   }
  9:   public void setJobEntityService(JobEntityService jobEntityService) {
 10:     this.jobEntityService = jobEntityService;
 11:   }
 12: 
 13: 
 14: }
  1: <bean id="demoSpringJob" class="com.xia.quartz.job.DemoSpringJob">
  2:     <property name="jobEntityService" ref="jobEntityService"></property>
  3:   </bean>

 

3.2 实体转换成任务Quartz.jobdetail

  1: private JobDetail convertJob(JobEntity jobEntity) throws ClassNotFoundException, NoSuchMethodException {
  2:     logger.debug("Job生成中:" + jobEntity.getJobName());
  3: 
  4:     JobDetail jobDetail;
  5:     if (jobEntity.isJobClassIsBeanName()) {//如果是spring bean
  6:       InvokerJobBean bean=ApplicationContextHolder.getBean("invokerJobBean");//通过invokerJobBean转换
  7:       bean.setTargetBeanName(jobEntity.getJobClass());
  8:       bean.setTargetMethod(jobEntity.getJobMethod());
  9:       bean.afterPropertiesSet();
 10:       jobDetail=bean.getObject();
 11:     } else {//如果是继承的是Job类
 12:       String jobClass = jobEntity.getJobClass();
 13:       @SuppressWarnings("unchecked")
 14:       Class<? extends Job> clazz = (Class<? extends Job>) Class.forName(jobClass);
 15:       jobDetail = ApplicationContextHolder.getBean("jobDetail");
 16:       jobDetail.setJobClass(clazz);
 17:     }
 18:     jobDetail.setName(jobEntity.getJobName());
 19:     jobDetail.setGroup(jobEntity.getJobGroupName());
 20:     jobDetail.setDescription(jobEntity.getJobDesc());
 21:     try {
 22:       jobDetail.getJobDataMap().putAll(jobEntity.getProperties());
 23:     } catch (Exception e) {
 24:       logger.error(e.getMessage());
 25:     }
 26:     return (JobDetail) jobDetail;
 27:   }

3.3 开始任务

在看此代码,你须了解Quartz的机制,它由三个东西组成:Scheduler,Trigger,JobDetail.对应作用是:

执行线程,执行策略,执行内容。也就是,在哪个线程下,使用什么策略,执行什么内容。执行线程下可以有多个Trigger,一个trigger下可以多个任务。

一般地,一个系统有一个Scheduler即可,而Trigger与JobDetail一般是一一对应的。毕竟不同任务执行周期都不同。

  1: public void startJob(JobEntity jobEntity) throws SchedulerException, ClassNotFoundException, NoSuchMethodException {
  2:     try {
  3:       JobDetail jobDetail = convertJob(jobEntity);
  4:       CronTrigger triggerCorn = convertTrigger(jobEntity);
  5:       if (StringUtils.isNotBlank(jobEntity.getJobGroupName())) {
  6:         jobEntity.setJobGroupName(jobDetail.getGroup());
  7:         jobEntityService.updateJobEntity(jobEntity);
  8:       }
  9:       //jobEntity.setStatus(JobStatus.WAITTING);
 10: 
 11:       Date ft = scheduler.scheduleJob(jobDetail, triggerCorn);
 12:       logger.debug("任务:" + jobDetail.getKey() + " will run at: " + ft.toLocaleString());
 13:     } catch (ParseException e) {
 14:       logger.error("任务转换失败:" + e.getMessage(), e);
 15:       throw new SchedulerException(e);
 16:     }
 17:   }

执行所有任务:

  1: QuartzService quartzService = ApplicationContextHolder.getBean("quartzService");
  2:     JobEntityService jobService = ApplicationContextHolder.getBean("jobEntityService");
  3:     List<JobEntity> all = jobService.getAllJobEntitys();
  4:     quartzService.startJobs(all);

3.3 状态变化

暂停的任务,如果暂停有执行点,那么,你继续后,同样会执行一次此任务。

其它状态变化在此没有列出。

  1: public void pauseJob(JobEntity jobEntity) throws SchedulerException {
  2:     logger.debug("暂停JOB:" + jobEntity);
  3:     scheduler.pauseJob(jobEntity.getJobName(), jobEntity.getJobGroupName());
  4:     // scheduler.interrupt(jobKey);
  5:     jobEntity.setStatus(JobStatus.PAUSED);
  6:     jobEntityService.updateJobEntity(jobEntity);
  7:   }

3.4数据库数据如下:

 

 

 

4.组件化

将quartz相关代码独立成一个工程,对外提供quartzFacade的服务即可,包括JobEntity的crud,pagingList,Exception view,Log view,ExceptionEvent push.

使用者要做的事就是,希望是开箱即用Out-Of-Box:

  • 引入工程
  • 增加spring配置
  • 注册一个ExceptionHandlerListener,
  • 使用quartzFacade服务

5.遗留问题

Spring bean 注入问题:

因Quartz的机制里,执行的任务类是这么jobDetail.setJobClass(clazz);注入进来的,那么,这个clazz如果要注入service,只能通过手工注入,或者使用使用spring

  1: <bean id="jobDetailInvoker"
  2:     class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  3:     <property name="targetObject" ref="nonQuartzJob" />
  4:     <property name="targetMethod">
  5:       <value>execute</value>
  6:     </property>
  7:   </bean>

而这个实现的原理是,把目标首丢过去,同时把要注入的东西丢到Quartz.job.contextMap中,在执行任务时,重装装配上去。

以下是Spring处理的相关代码

org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean:

  1: public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
  2:     prepare();
  3: 
  4:     // Use specific name if given, else fall back to bean name.
  5:     String name = (this.name != null ? this.name : this.beanName);
  6: 
  7:     // Consider the concurrent flag to choose between stateful and stateless job.
  8:     Class jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class);
  9: 
 10:     // Build JobDetail instance.
 11:     this.jobDetail = new JobDetail(name, this.group, jobClass);
 12:     this.jobDetail.getJobDataMap().put("methodInvoker", this);
 13:     this.jobDetail.setVolatility(true);
 14:     this.jobDetail.setDurability(true);
 15: 
 16:     // Register job listener names.
 17:     if (this.jobListenerNames != null) {
 18:       for (String jobListenerName : this.jobListenerNames) {
 19:         this.jobDetail.addJobListener(jobListenerName);
 20:       }
 21:     }
 22: 
 23:     postProcessJobDetail(this.jobDetail);
 24:   }
  1: public static class MethodInvokingJob extends QuartzJobBean {
  2: 
  3:     protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class);
  4: 
  5:     private MethodInvoker methodInvoker;
  6: 
  7:     /**
  8:      * Set the MethodInvoker to use.
  9:      */
 10:     public void setMethodInvoker(MethodInvoker methodInvoker) {
 11:       this.methodInvoker = methodInvoker;
 12:     }
 13: 
 14:     /**
 15:      * Invoke the method via the MethodInvoker.
 16:      */
 17:     @Override
 18:     protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
 19:       try {
 20:         context.setResult(this.methodInvoker.invoke());
 21:       }
 22:       catch (InvocationTargetException ex) {
 23:         if (ex.getTargetException() instanceof JobExecutionException) {
 24:           // -> JobExecutionException, to be logged at info level by Quartz
 25:           throw (JobExecutionException) ex.getTargetException();
 26:         }
 27:         else {
 28:           // -> "unhandled exception", to be logged at error level by Quartz
 29:           throw new JobMethodInvocationFailedException(this.methodInvoker, ex.getTargetException());
 30:         }
 31:       }
 32:       catch (Exception ex) {
 33:         // -> "unhandled exception", to be logged at error level by Quartz
 34:         throw new JobMethodInvocationFailedException(this.methodInvoker, ex);
 35:       }
 36:     }
 37:   }
  1: public abstract class QuartzJobBean implements Job {
  2: 
  3:   /**
  4:    * This implementation applies the passed-in job data map as bean property
  5:    * values, and delegates to <code>executeInternal</code> afterwards.
  6:    * @see #executeInternal
  7:    */
  8:   public final void execute(JobExecutionContext context) throws JobExecutionException {
  9:     try {
 10:       BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
 11:       MutablePropertyValues pvs = new MutablePropertyValues();
 12:       pvs.addPropertyValues(context.getScheduler().getContext());
 13:       pvs.addPropertyValues(context.getMergedJobDataMap());
 14:       bw.setPropertyValues(pvs, true);
 15:     }
 16:     catch (SchedulerException ex) {
 17:       throw new JobExecutionException(ex);
 18:     }
 19:     executeInternal(context);
 20:   }

posted on 2022-05-24 17:23  癫狂编程  阅读(1217)  评论(0编辑  收藏  举报

导航

好的代码像粥一样,都是用时间熬出来的