Java - Quarz 定时任务_使用注意点
前言
在开发过程中,我们会用定时任务来执行一些操作,例如定时去捞取流水重试业务、定时去消息中间件获取消息等等相关需求
简单的定时任务实现可以借助Spring提供的 @Scheduled 注解 详细看 Spring 原理之 Scheduled
如果涉及到 定时任务的动态管理就需要使用到其他技术,下面介绍一下Quartz
Quartz是一个开源的任务日程管理系统, 由 OpenSymphony开源,同时它是一个功能丰富的任务调用系统,可创建简单或者复杂的几十、几百、甚至成千上万的job。除此之外,quartz调度器还支持JTA事务和集群。
Maven 的pom.xml文件引入相关依赖包
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
Quarz使用注意点
本篇博客接着上篇 Java - Quarz 定时任务,上篇主要介绍一下Quartz 的基本使用,内容也挺多了
这篇主要说一下 Quartz 使用的一些注意点
1、Job 的使用
1.1 自定义 Job 需要无参构造方法(必须)
在上篇说到,在 Scheduler 的 scheduleJob (配置定时任务) 方法中,设置了相关的 JobDetail 实例
而在创建JobDetail时,将 要执行的job的类名 传给了JobDetail,所以scheduler就知道了要执行何种类型的job,例如:
private JobDetail createJobDetail(QuartzJob quartzJob){
String JOB_NAME = "TASK_";
return JobBuilder.newJob(ExecuteJob.class).
withIdentity(JOB_NAME + quartzJob.getId()).build();
}
这里的ExecuteJob类为自定义的Job,实现Job接口,如下:
public class ExecuteJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getMergedJobDataMap();
String AString = dataMap.getString("AString");
Float BFloatValue = dataMap.getFloat("BFloatValue");
System.out.println("Instance " + key + " of AString : " + AString + ", and BFloatValue is: " + BFloatValue);
}
}
那么每次当 scheduler 执行job时,就会调用 execute(…) 方法执行相应的业务逻辑
注意:在调用其 execute(…) 方法之前会创建该类的一个新的实例,处理逻辑如下:
public Job newJob(TriggerFiredBundle bundle, Scheduler Scheduler) throws SchedulerException {
JobDetail jobDetail = bundle.getJobDetail();
Class<? extends Job> jobClass = jobDetail.getJobClass();
try {
if(log.isDebugEnabled()) {
log.debug( "Producing instance of Job '" + jobDetail.getKey() + "', class=" + jobClass.getName());
}
return jobClass.newInstance();
} catch (Exception e) {
SchedulerException se = new SchedulerException("Problem instantiating class '" + jobDetail.getJobClass().getName() + "'", e);
throw se;
}
}
可以看到,其是通过 jobClass.newInstance(); 来实例化对象的,那么 必须要有无参构造方法 ,如果 ExecuteJob 没有 有参构造方法 时,其会默认有一个无参构造方法,所以上面的代码不会出现问题
如果ExecuteJob 变为这样:
public class ExecuteJob implements Job {
private String a;
ExecuteJob(String s) {
this.a = s;
}
public void execute(JobExecutionContext context)
throws JobExecutionException {
.....与上面相同....
}
}
再次运行的时候,就会报以下错误:(大家可以试试)
org.quartz.SchedulerException: Problem instantiating class ‘com.study.DO.ExecuteJob’
1.2 自定义 Job 定义的有状态的数据属性值不会保留(注意点)
在job的多次执行中,这些属性的值不会保留
这个根据1.1 的解释 很通俗易懂了把,因为执行每次都是通过 newInstance 方法创建一个新的实例,所以你之前的Job 的属性值肯定不可能保留
如果多次执行Job 想跟踪Job的状态,可以借助 JobDataMap,JobDetail对象的一部分,设置一个Map来存储
1.3 JobDataMap 设置 Job 的数据属性值(便捷使用点)
在 ExecuteJob 中,我们通过 JobDataMap 设置了我们想追踪的Key值,然后在execute 方法中来获取到进行相应的处理,例如:
public class ExecuteJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getMergedJobDataMap();
String AString = dataMap.getString("AString");
Float BFloatValue = dataMap.getFloat("BFloatValue");
System.out.println("Instance " + key + " of AString : " + AString + ", and BFloatValue is: " + BFloatValue);
}
}
private JobDetail createJobDetail(QuartzJob quartzJob) {
String JOB_NAME = "TASK_";
return JobBuilder.newJob(ExecuteJob.class).
withIdentity(JOB_NAME + quartzJob.getId())
.usingJobData("AString", "i am a string ")
.usingJobData("BFloatValue", 0F)
.build();
}
除了上面那种方式,我们还可以简化 execute 中的代码,通过 set 方法来获取
public class ExecuteJob implements Job {
private String aString;
private Float bFloatValue;
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getMergedJobDataMap();
System.out.println("Instance " + key + " of AString : " + aString + ", and BFloatValue is: " + bFloatValue);
}
public void setAString(String aString) {
this.aString = aString;
}
public void setBFloatValue(Float bFloatValue) {
this.bFloatValue = bFloatValue;
}
}
这样的好处是,如果 execute 中有大量获取 JobDataMap 中的代码,我们都可以省略。(有人可能说了,这里的代码不还是多了嘛~)
这里多的代码无非是 set 相关的代码,我们完全可以借助 lombok,关于 lombok可以查看另外一篇博客:lombok 的使用,这样的话,不就大大减少了代码量嘛~
package com.study.DO;
import lombok.Data;
import lombok.Setter;
import org.quartz.*;
@Setter
public class ExecuteJob implements Job {
private String aString;
private Float bFloatValue;
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getMergedJobDataMap();
System.out.println("Instance " + key + " of AString : " + aString + ", and BFloatValue is: " + bFloatValue);
}
}
其原理是:(Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了),源码如下:
public class PropertySettingJobFactory extends SimpleJobFactory {
private boolean warnIfNotFound = false;
private boolean throwIfNotFound = false;
@Override
public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
Job job = super.newJob(bundle, scheduler);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.putAll(scheduler.getContext());
jobDataMap.putAll(bundle.getJobDetail().getJobDataMap());
jobDataMap.putAll(bundle.getTrigger().getJobDataMap());
setBeanProps(job, jobDataMap);
return job;
}
protected void setBeanProps(Object obj, JobDataMap data) throws SchedulerException {
BeanInfo bi = null;
try {
bi = Introspector.getBeanInfo(obj.getClass());
} catch (IntrospectionException e) {
handleError("Unable to introspect Job class.", e);
}
PropertyDescriptor[] propDescs = bi.getPropertyDescriptors();
for (Iterator<?> entryIter = data.getWrappedMap().entrySet().iterator(); entryIter.hasNext();) {
Map.Entry<?,?> entry = (Map.Entry<?,?>)entryIter.next();
String name = (String)entry.getKey();
String c = name.substring(0, 1).toUpperCase(Locale.US);
String methName = "set" + c + name.substring(1);
java.lang.reflect.Method setMeth = getSetMethod(methName, propDescs);
Class<?> paramType = null;
Object o = null;
try {
if (setMeth == null) {
handleError( "No setter on Job class " + obj.getClass().getName() + " for property '" + name + "'");
continue;
}
paramType = setMeth.getParameterTypes()[0];
o = entry.getValue();
Object parm = null;
if (paramType.isPrimitive()) {
if (o == null) {
handleError( "Cannot set primitive property '" + name + "' on Job class " + obj.getClass().getName() + " to null.");
continue;
}
if (paramType.equals(int.class)) {
if (o instanceof String) {
parm = Integer.valueOf((String)o);
} else if (o instanceof Integer) {
parm = o;
}
} else if (paramType.equals(long.class)) {
if (o instanceof String) {
parm = Long.valueOf((String)o);
} else if (o instanceof Long) {
parm = o;
}
} else if (paramType.equals(float.class)) {
if (o instanceof String) {
parm = Float.valueOf((String)o);
} else if (o instanceof Float) {
parm = o;
}
} else if (paramType.equals(double.class)) {
if (o instanceof String) {
parm = Double.valueOf((String)o);
} else if (o instanceof Double) {
parm = o;
}
} else if (paramType.equals(boolean.class)) {
if (o instanceof String) {
parm = Boolean.valueOf((String)o);
} else if (o instanceof Boolean) {
parm = o;
}
} else if (paramType.equals(byte.class)) {
if (o instanceof String) {
parm = Byte.valueOf((String)o);
} else if (o instanceof Byte) {
parm = o;
}
} else if (paramType.equals(short.class)) {
if (o instanceof String) {
parm = Short.valueOf((String)o);
} else if (o instanceof Short) {
parm = o;
}
} else if (paramType.equals(char.class)) {
if (o instanceof String) {
String str = (String)o;
if (str.length() == 1) {
parm = Character.valueOf(str.charAt(0));
}
} else if (o instanceof Character) {
parm = o;
}
}
} else if ((o != null) && (paramType.isAssignableFrom(o.getClass()))) {
parm = o;
}
if ((o != null) && (parm == null)) {
handleError(
"The setter on Job class " + obj.getClass().getName() +
" for property '" + name +
"' expects a " + paramType +
" but was given " + o.getClass().getName());
continue;
}
setMeth.invoke(obj, new Object[]{ parm });
} catch (NumberFormatException nfe) {
handleError(
"The setter on Job class " + obj.getClass().getName() +
" for property '" + name +
"' expects a " + paramType +
" but was given " + o.getClass().getName(), nfe);
} catch (IllegalArgumentException e) {
handleError(
"The setter on Job class " + obj.getClass().getName() +
" for property '" + name +
"' expects a " + paramType +
" but was given " + o.getClass().getName(), e);
} catch (IllegalAccessException e) {
handleError(
"The setter on Job class " + obj.getClass().getName() +
" for property '" + name +
"' could not be accessed.", e);
} catch (InvocationTargetException e) {
handleError(
"The setter on Job class " + obj.getClass().getName() +
" for property '" + name +
"' could not be invoked.", e);
}
}
}
}
1.4 Job状态与并发(建议两个注解同时使用)
关于job的状态数据(即JobDataMap)和并发性,有一些地方需要注意,我们可以在job类上可以加入一些注解,这些注解会影响job的状态和并发性
Job 与 JobDetail 是 1对多 的关系,即我们创建了一个 Job,可以基于这个 Job 创建出多个 JobDetail,每一个 JobDetail 都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中进行执行。一个 Job 的多个 JobDetail 实例 涉及到了 并发 的问题
- @DisallowConcurrentExecution:将该注解加到 job类 上,告诉 Quartz 不要并发地执行同一个 Job 的多个 JobDetail 实例。所以该限制是针对JobDetail的,而不是job类的。但是应该将该注解放在job类上,因为 job类 的改变经常会导致其行为发生变化。
- @PersistJobDataAfterExecution:将该注解加在job类上,告诉 Quartz 在成功执行了 job类 的 execute方法 后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。尽管注解是加在job类上的,但其限制作用是针对job实例的,而不是 job类 的
在 JobDetail 中,有以下两个方法来判断是否你加了注解:
public boolean isPersistJobDataAfterExecution();
public boolean isConcurrentExectionDisallowed();
建议 若使用了@PersistJobDataAfterExecution注解,要同时使用@DisallowConcurrentExecution注解。因为当同一个job(JobDetail)的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的(JobDataMap底层是HashMap,HashMap是线程不安全的)
总结
这里说明了几点关于Quartz 使用的注意点,在使用的过程中需要注意,关于 Quartz 的 整个原理与过程会再额外整理一下~
|