Java - Quarz 定时任务_使用注意点

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 需要无参构造方法(必须)

在上篇说到,在 SchedulerscheduleJob (配置定时任务) 方法中,设置了相关的 JobDetail 实例

而在创建JobDetail时,将 要执行的job的类名 传给了JobDetail,所以scheduler就知道了要执行何种类型的job,例如:

// quartzJob 是与数据库相关的DO,用其id来构造JobKey保证唯一性
private JobDetail createJobDetail(QuartzJob quartzJob){
      String JOB_NAME = "TASK_"; // JobKey的名称前缀
      return JobBuilder.newJob(ExecuteJob.class). // 指定Job 的JobBuilder 
                withIdentity(JOB_NAME + quartzJob.getId()).build(); // 创建
}

这里的ExecuteJob类为自定义的Job,实现Job接口,如下:

public class ExecuteJob implements Job {

    // context 里面包含了很多方法,常用的如下
    // getScheduler:获取Scheduler
    // getTrigger:获取触发器Trigger
    //getJobDetail:获取JobDetail
    //getJobInstance:获取到Job
    // getMergedJobDataMap:获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值
    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        // 获取 JobDetail 的 JobKey
        JobKey key = context.getJobDetail().getKey();
        // 获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集
        JobDataMap dataMap = context.getMergedJobDataMap();
        // 获取创建 JobDetail 设置的值
        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(…) 方法之前会创建该类的一个新的实例,处理逻辑如下:

// org.quartz.simpl.SimpleJobFactory#newJob

public Job newJob(TriggerFiredBundle bundle, Scheduler Scheduler) throws SchedulerException {
    JobDetail jobDetail = bundle.getJobDetail();
    // 获取这个 Job 的class
    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 就不会有无参构造方法
    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 {

    // context 里面包含了很多方法,常用的如下
    // getScheduler:获取Scheduler
    // getTrigger:获取触发器Trigger
    //getJobDetail:获取JobDetail
    //getJobInstance:获取到Job
    // getMergedJobDataMap:获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值
    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        // 获取 JobDetail 的 JobKey
        JobKey key = context.getJobDetail().getKey();
        // 获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集
        JobDataMap dataMap = context.getMergedJobDataMap();
        // 获取创建 JobDetail 设置的值
        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;

    // context 里面包含了很多方法,常用的如下
    // getScheduler:获取Scheduler
    // getTrigger:获取触发器Trigger
    //getJobDetail:获取JobDetail
    //getJobInstance:获取到Job
    // getMergedJobDataMap:获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值
    public void execute(JobExecutionContext context)
            throws JobExecutionException {

        // 获取 JobDetail 的 JobKey
        JobKey key = context.getJobDetail().getKey();
        // 获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集
        JobDataMap dataMap = context.getMergedJobDataMap();
        // 获取创建 JobDetail 设置的值
//        String aString = dataMap.getString("AString");
//        Float bFloatValue = dataMap.getFloat("BFloatValue");
        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.*;

//@Data
@Setter
public class ExecuteJob implements Job {
    private String aString;
    private Float bFloatValue;
    public void execute(JobExecutionContext context)
            throws JobExecutionException {
        // 获取 JobDetail 的 JobKey
        JobKey key = context.getJobDetail().getKey();
        // 获取 JobDetail中的JobDataMap和Trigger中的JobDataMap的并集
        JobDataMap dataMap = context.getMergedJobDataMap();
        // 获取创建 JobDetail 设置的值
//        String aString = dataMap.getString("AString");
//        Float bFloatValue = dataMap.getFloat("BFloatValue");

        System.out.println("Instance " + key + " of AString : " + aString + ", and BFloatValue is: " + bFloatValue);
    }

}

原理是:(Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了),源码如下:

// org.quartz.simpl.PropertySettingJobFactory

public class PropertySettingJobFactory extends SimpleJobFactory {
    private boolean warnIfNotFound = false;
    private boolean throwIfNotFound = false;
    
    @Override
    public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
		// 创建Job newInstance()
        Job job = super.newJob(bundle, scheduler);
        
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.putAll(scheduler.getContext());
        jobDataMap.putAll(bundle.getJobDetail().getJobDataMap());
        jobDataMap.putAll(bundle.getTrigger().getJobDataMap());
		// 设置属性值,通过set方法
        setBeanProps(job, jobDataMap);
        
        return job;
    }
    // 设置属性值,通过set方法  
    // 很常见的实现逻辑
    // 根据class 获取到所有的参数,然后根据参数查找是否有set方法,如果有,就根据set方法设置值  
    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 the parameter wasn't originally null, but we didn't find a 
                // matching parameter, then we are stuck.
                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 中,有以下两个方法来判断是否你加了注解:

/**
 * @see PersistJobDataAfterExecution
 * @return whether the associated Job class carries the {@link PersistJobDataAfterExecution} annotation.
 */
public boolean isPersistJobDataAfterExecution();

/**
 * @see DisallowConcurrentExecution
 * @return whether the associated Job class carries the {@link DisallowConcurrentExecution} annotation.
 */
public boolean isConcurrentExectionDisallowed();

建议 若使用了@PersistJobDataAfterExecution注解,要同时使用@DisallowConcurrentExecution注解。因为当同一个job(JobDetail)的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的(JobDataMap底层是HashMap,HashMap是线程不安全的

总结

这里说明了几点关于Quartz 使用的注意点,在使用的过程中需要注意,关于 Quartz 的 整个原理与过程会再额外整理一下~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值