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数据库数据如下:
![](https://img2022.cnblogs.com/blog/1107379/202205/1107379-20220524172248009-258516131.png)
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: }