还在用Timer吗?Quertz可以试一试

简介

Quertz是一个开源的作业任务调度框架,他可以完成像JavaScript定时器类式的功能,其实Java中Timer也可实现部分功能,但相比Quertz还是略逊一筹。
Quartz 核心元素

  • Scheduler——任务调度器
  • Trigger——触发器
  • Job——任务
  • trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器
  • Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。
  • Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
  • Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。

使用

(1)添加依赖


    org.quartz-scheduler
    quartz
    2.2.1
  
  
    org.quartz-scheduler
    quartz-jobs
    2.2.1
   

(2)自定义任务工厂
覆盖SpringBeanJobFactory中的createJobInstance(), 对其创建出来的类自动注入
(可以嵌在配置类作为静态内部类,然后实例化对象赋值)

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import org.springframework.stereotype.Component;
 
//继承SpringBeanJobFactory 实现任务实例化方式
//ApplicationContextAware 通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationContext方法
@Component    //把普通pojo实例化到spring容器中
public class CustomJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
 
    private transient AutowireCapableBeanFactory beanFactory;
 
 	//将job实例交给spring ioc容器管理
    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
 
        return job;
    }
    
    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }
 
}

(3)添加配置类
定义configuration并暴露bean给spring IOC

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 
@Configuration
@EnableScheduling   //开启Spring 的schedule task执行的能力,一般作用于配置类
public class SchedulerConfiguration {
 
    @Autowired
    private ApplicationContext applicationContext;
    // 配置文件路径
    static final String QUARTZ_CONFIG = "/quartz.properties";
 
    @Autowired
    private CustomJobFactory customJobFactory ;
	 @Bean
       public JobFactory jobFactory(ApplicationContext applicationContext) {
            	//自定义工厂整合ApplicationContext 
            	//return new CustomJobFactory (),
            	//可省略下面的代码,因为spring加载时自动调用setApplicationContext
                CustomJobFactory jobFactory = new CustomJobFactory ();
                jobFactory.setApplicationContext(applicationContext);
                return jobFactory;
        }

        //配置任务调度器
        @Bean				//注解的方法参数默认@Autowired
        public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource)
                        throws Exception {
                SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
                // 将自定义job工厂交给调度器来维护
                schedulerFactoryBean.setJobFactory(jobFactory);
                //用于Quartz集群,启动时更新(覆盖)已存在的Job
                schedulerFactoryBean.setOverwriteExistingJobs(true);
                // 项目启动完成后,等待2秒后开始执行调度器初始化
                schedulerFactoryBean.setStartupDelay(2);
                // 设置调度器自动运行
                schedulerFactoryBean.setAutoStartup(true);
                // 设置数据源,使用与项目统一数据源
                schedulerFactoryBean.setDataSource(dataSource);
                // 设置上下文spring bean name
                schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
                // 设置配置文件位置
                schedulerFactoryBean.setConfigLocation(new ClassPathResource(QUARTZ_CONFIG));
                return schedulerFactoryBean;
        }
}

(4)Quartz 对于定时任务,提供了一个Job接口。所有你需要实现的定时任务都需要实现Job接口。

/**
*job执行入口
**/
public class BaseJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //do something you want to do
        System.out.println("job 1 start!!!");
    }

}

public class TestJob implements BaseJob {

	public TestJob() {
	}

	private void initService() {
	}

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {

		// JobDetail中的JobDataMap是共用的,从getMergedJobDataMap获取的JobDataMap是全新的对象
		// JobDataMap map = context.getMergedJobDataMap();
		initService();
		JobDataMap map = context.getJobDetail().getJobDataMap();
		try {
			map.get("params").toString();
			//TODO
		} catch (Exception e1) {
			logger.info("{}", e1);
			e1.printStackTrace();
		}
	}
}

(5)任务基本属性,可构建实体类并持久化到数据库中。

String jobName;								//任务名称
String jobGroupName; 						//任务组名称	
String triggerName;							//触发器名称
String triggerGroupName;					//触发器组名称
String jobStatus;							//任务状态
String cronExpression;						//任务表达式
String createTime;							//创建时间
String classPath;							//任务类类型
String params;								//job参数
String description;								//任务描述

(6)定时任务service实现

  • Job:任务
  • JobBuilder:任务构建
  • TriggerBuilder:触发构建对象
  • CronScheduleBuilder:表达式任务构建器
  • TriggerKey:触发key名称
  • JobDataMap :业务参数传递对象
  • @Autowired private Scheduler scheduler; --注入Scheduler 对象
  • JobBuilder.newJob(cls).withIdentity(jobName, JOB_GROUP_NAME).build(); 用于描叙Job实现类及其他的一些静态信息,构建一个作业实例
  • (CronTrigger) TriggerBuilder.newTrigger() 创建一个新的TriggerBuilder来规范一个触发器
  • .withIdentity(jobName, TRIGGER_GROUP_NAME) 给触发器起一个名字和组名
@Autowired
private Scheduler scheduler;
/**
 * 功能: 添加一个定时任务
 * @param jobName 任务名(同一个任务组中唯一)
 * @param jobGroupName 任务组名
 * @param triggerName 触发器名(同一个触发器组中唯一)
 * @param triggerGroupName 触发器组名
 * @param jobClass 任务的类类型 eg:TimedMassJob.class
 * @param cron 时间设置 表达式,参考quartz说明文档
 * @param objects 可变参数需要进行传参的值
 */
public void addjob(String jobName, String jobGroupName, String triggerName,String triggerGroupName, Class jobClass, String cron, Object... objects){
    try {
    	//job实例属性,通过JobBuilder类创建
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName).build();
        if(objects != null){
            Integer i = 0;
            for (Object obj: objects) {
            	//JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,
            	//可以使用其中的数据;JobDataMap是Java Map接口的一个实现,
            	//额外增加了一些便于存取基本类型的数据的方法。
                jobDetail.getJobDataMap().put("data"+i, obj);
                i++;
            }
        }
        //TriggerBuilder是一个建造者模式,链式建造。通过静态方法构建一个TriggerBuilder实例,
        //然后再调用类方法Build()创建一个ITrigger的实现。
        TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger();
        triggerBuilder.withIdentity(triggerName, triggerGroupName);
        triggerBuilder.startNow();
        triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
        //CronTrigger通常比Simple Trigger更有用,如果您需要基于日历的概念而不是按照SimpleTrigger的精确指定间隔进行重新启动的作业启动计划。
        CronTrigger cronTrigger = (CronTrigger)triggerBuilder.build();
        scheduler.scheduleJob(jobDetail, cronTrigger);
        if(!scheduler.isShutdown()){
            scheduler.start();
        }
    } catch (SchedulerException e) {
        logger.error("error:\n",e);
    }
}

/**
 * 功能: 移除一个任务
 * @param jobName
 * @param jobGroupName
 * @param triggerName
 * @param triggerGroupName
 */
public  void removeJob(String jobName, String jobGroupName,String triggerName, String triggerGroupName){
    try {
    	//Trigger身份对象
        TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
        //停止触发器
        scheduler.pauseTrigger(triggerKey);
        //移除触发器
        scheduler.unscheduleJob(triggerKey);
        //删除任务
        //JobKey:Job身份对象
        scheduler.deleteJob(JobKey.jobKey(jobName,jobGroupName));
    } catch (SchedulerException e) {
        logger.error("error:\n",e);
    }
}

/**
 * 功能:修改一个任务的触发时间
 * @param triggerName 触发器名
 * @param triggerGroupName 触发器组名
 * @param cron 时间设置,参考quartz说明文档
 */
public void modifyJobTime(String triggerName,String triggerGroupName, String cron){
    try {
        TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
        CronTrigger trigger = (CronTrigger)scheduler.getTrigger(triggerKey);
        if(trigger == null){
            return;
        }
        String oldTime = trigger.getCronExpression();
        if(!oldTime.equalsIgnoreCase(cron)){
            TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger();
            triggerBuilder.withIdentity(triggerName, triggerGroupName);
            triggerBuilder.startNow();
            //CronTrigger使用Cron表达是来定义任务的触发时间,CronTrigger由CronScheduleBuilder构建而成
            triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
            trigger = (CronTrigger) triggerBuilder.build();
            scheduler.rescheduleJob(triggerKey, trigger);
        }
    } catch (SchedulerException e) {
        logger.error("error:\n",e);
    }
}

/**
  * 暂停定时任务
  *
  * @param jobName
  * @param jobGroup
*/
@Override
public void pause(String jobName, String jobGroup) {
    TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
    try {
        if (checkExists(jobName, jobGroup)) {
            scheduler.pauseTrigger(triggerKey);
            logger.info("pause job success, triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName);
        }
    } catch (SchedulerException e) {
            logger.error(e.getMessage());
    }
  }

 /**
     * 重新开始任务
     *
     * @param jobName
     * @param jobGroup
 */
@Override
public void resume(String jobName, String jobGroup) {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);

        try {
            if (checkExists(jobName, jobGroup)) {
                scheduler.resumeTrigger(triggerKey);
                logger.info("resume job success,triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName);
            }
        } catch (SchedulerException e) {
            logger.error(e.getMessage());
        }
 }

/**
     * 验证任务是否存在
     *
     * @param jobName
     * @param jobGroup
     * @throws SchedulerException
*/
@Override
public boolean checkExists(String jobName, String jobGroup) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
        return scheduler.checkExists(triggerKey);
}

/**
* 获取JobDetail
*/
@Override
public JobDetail geJobDetail(JobKey jobKey, String className, String description, JobDataMap map)
            throws Exception {
        return JobBuilder.newJob(getClass(className)).withIdentity(jobKey).withDescription(description)
                .setJobData(map).storeDurably().build();
}

 /**
* 获取Trigger (Job的触发器,执行规则) (所有misfire的任务会马上执行)
*/
@Override
public Trigger getTrigger(SysJob job) {
        return TriggerBuilder.newTrigger().withIdentity(job.getCronName(), job.getGroupName())
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCron())
                        .withMisfireHandlingInstructionFireAndProceed())
                .build();
}

/**
* 启动所有任务
**/
public  void startJobs(){
    try {
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.start();
    } catch (SchedulerException e) {
        logger.error("error:\n",e);
    }
}

/**
* 关闭所有任务
**/
public void shutdownJobs(){
    try {
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.shutdown();
    } catch (SchedulerException e) {
        logger.error("error:\n",e);
    }
}

(7)工具类

public class JobCronUtil {

        /**
         *  根据执行时间,创建quarz cron表达式
         * @param jobTime 任务执行时间
         */
        public static String createCron(Date jobTime) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(jobTime);
                StringBuffer cronBuff = new StringBuffer();
                cronBuff.append(calendar.get(Calendar.SECOND)).append(" ");
                cronBuff.append(calendar.get(Calendar.MINUTE)).append(" ");
                cronBuff.append(calendar.get(Calendar.HOUR_OF_DAY)).append(" ");
                cronBuff.append(calendar.get(Calendar.DAY_OF_MONTH)).append(" ");
                cronBuff.append(calendar.get(Calendar.MONTH) + 1).append(" ");
                cronBuff.append(" ? ");
                //cronBuff.append(calendar.get(Calendar.YEAR)+"-"+calendar.get(Calendar.YEAR)).append(" ");
                return cronBuff.toString();
        }

        /**
         * 根据执行时间、时间间隔,创建 quarz cron表达式
         * @param jobBeginTime 任务开始时间
         * @param interval 间隔长度
         * @param intervalUnit  间隔单位
         */
        public static String createCron(Date jobBeginTime, Integer interval, TimeUnit intervalUnit) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(jobBeginTime);
                StringBuffer cronBuff = new StringBuffer();
                cronBuff.append(calendar.get(Calendar.SECOND)).append(" ");
                cronBuff.append(calendar.get(Calendar.MINUTE));
                if (interval != null && TimeUnit.minute.equals(intervalUnit)) {
                        cronBuff.append("/" + interval);
                }
                cronBuff.append(" ").append(calendar.get(Calendar.HOUR_OF_DAY));
                if (interval != null && TimeUnit.hour.equals(intervalUnit)) {
                        cronBuff.append("/" + interval);
                }
                cronBuff.append(" ").append(calendar.get(Calendar.DAY_OF_MONTH));
                if (interval != null && TimeUnit.day.equals(intervalUnit)) {
                        cronBuff.append("/" + interval);
                }
                cronBuff.append(" ").append(calendar.get(Calendar.MONTH) + 1).append(" ");
                cronBuff.append(" ?  ");
                //cronBuff.append(calendar.get(Calendar.YEAR)+"-"+calendar.get(Calendar.YEAR)).append(" ");
                return cronBuff.toString();
        }
}

(8)quartz.properties

#quartz集群配置
# ===========================================================================
# Configure Main Scheduler Properties 调度器属性
# ===========================================================================
#调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceid=AUTO
#禁用quartz软件更新
org.quartz.scheduler.skipUpdateCheck=true
#============================================================================
# Configure ThreadPool
#============================================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount = 25
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority = 5
#============================================================================
# Configure JobStore
#============================================================================
# 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold = 60000
#数据保存方式为数据库持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#JobDataMaps是否都为String类型
org.quartz.jobStore.useProperties = false
#数据库别名 随便取
org.quartz.jobStore.dataSource = myDS
#表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix = QRTZ_
#是否加入集群
org.quartz.jobStore.isClustered = true
#调度实例失效的检查时间间隔
org.quartz.jobStore.clusterCheckinInterval = 20000
#============================================================================
# Configure Datasources
#============================================================================
#自定义连接池
org.quartz.dataSource.myDS.connectionProvider.class=cn.lsr.quartz.config.DruidConnectionProvider
#数据库引擎
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
#数据库连接
org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/lsr-microservice?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
#数据库用户
org.quartz.dataSource.myDS.user = root
#数据库密码
org.quartz.dataSource.myDS.password = lishirui
#允许最大连接
org.quartz.dataSource.myDS.maxConnections = 5
#验证查询sql,可以不设置
org.quartz.dataSource.myDS.validationQuery=select 0 from dual
#注:如果嫌需要额外配置quart数据源很烦,也可以共用你项目配置的数据库链接,这样每次更换数据库连接,就不需要额外在修改。

(9)cron表达式

Quartz里面有一个cron表达式,用来设置定时任务执行的时间
1.Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: 
Seconds Minutes Hours DayofMonth Month DayofWeek Year或 Seconds Minutes Hours DayofMonth Month DayofWeek

2.每一个域可出现的字符如下: 
Seconds:可出现", - * /"四个字符,有效范围为0-59的整数 
Minutes:可出现", - * /"四个字符,有效范围为0-59的整数 
Hours:可出现", - * /"四个字符,有效范围为0-23的整数 
DayofMonth:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数 
Month:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc 
DayofWeek:可出现", - * / ? L C #"四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推 
Year:可出现", - * /"四个字符,有效范围为1970-2099年
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

(1)*:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。
     例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 
其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
(3)-: 表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次 。
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.。
(5),: 表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(6)L: 表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(7)W: 表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在        
      DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;
      如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。
(8)LW: 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。 
(9)#: 用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

举几个例子: 
0 0 2 1 * ? * 表示在每月的1日的凌晨2点调度任务 
0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 
0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。 
按顺序依次为 
秒(0~59) 
分钟(0~59) 
小时(0~23) 
天(月)(0~31,但是你需要考虑你月的天数) 
月(0~11) 
天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT) 
年份(1970-2099
其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 
0 0 12 ? * WED 表示每个星期三中午12点 
"0 0 12 * * ?" 每天中午12点触发 
"0 15 10 ? * *" 每天上午10:15触发 
"0 15 10 * * ?" 每天上午10:15触发 
"0 15 10 * * ? *" 每天上午10:15触发 
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发 
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 
"0 15 10 15 * ?" 每月15日上午10:15触发 
"0 15 10 L * ?" 每月最后一日的上午10:15触发 
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

 
3. 有些子表达式能包含一些范围或列表。
例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”
“*”字符代表所有可能的值
因此,“*”在子表达式(月)里表示每个月的含义,“*”在子表达式(天(星期))表示星期的每一天

4. “/”字符用来指定数值的增量 
例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟 
在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

 

5. “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值 
当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”
“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写,但是它在两个子表达式里的含义是不同的。 
在天(月)子表达式中,“L”表示一个月的最后一天 
在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT
如果在“L”前有具体的内容,它就具有其他的含义了
例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 
注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题。

参考:GO GO GO

你可能感兴趣的:(java)