掐指算来, IOC+AOP两大神器看完之后, 补充了SPEL, DAO的事务和具体类使用, Spring Cache的知识, 现在还差一个, 就是Web应用很多时候需要定时的, 异步的任务, 比如发送电子邮件等任务, 进行统计, 与用户的请求和响应没有直接关系. 这也是一个Web应用不可或缺的部分.

在开始使用Spring Web MVC之前, 异步任务是最后一个知识点了. 简单看了一下这章内容, 将接触之前从来没有用过的Java的一些异步任务调度框架, 比如Quartz, JDK Timer等, 每个工具背后也都是一个崭新的世界, 一起来探索吧.

另外,虽然马上要开始最后的Spring MVC了, 但是还需要开启两条重要的支线,就是PostgreSQL和Hibernate。 一个DBA朋友推荐了PostgreSQL的一个很好的Git项目,乃是集大成的PgSQL的内容。Hibernate也有一个Git持久化教程,之后就准备看这两个加上官网文档了。

  1. Quartz
  2. 使用SimpleTrigger的简单例子
  3. CronTrigger
  4. Calendar的使用

Quartz

在本章之前, 别说用了, 我基本都没有听说过Quartz, 毕竟经验太少, 还没有玩过什么真正的多线程异步程序.

实际应用中可能都会碰到任务调度的需求, 比如定期统计一些东西, 这些任务的核心都是以时间为条件. Java本身从1.3开始, 提供了java.util.Timer和TimerTask, 可以用来进行简单的调度任务.

在调度任务方面Quartz是非常强大又不失简单性的一个任务调度框架. 官网是http://www.quartz-scheduler.org/.

写文章的时候看到最新稳定版本是2.3版, 2.4版正在测试中. 依赖如下:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

Quartz我看了一遍书, 基本上明白整体结构了.

就像IOC容器一样, Quartz有一个运行时的容器类, 叫做Scheduler. 通过SchedulerFactory可以创建一个Scheduler对象, 每个Scheduler对象有一个SchedulerContext. 像不像Servlet容器, 像不像IOC容器.

在容器里边, 因为是触发任务, 所以容器里边有两大类东西: 一类是Trigger, 触发器, 描述如何触发任务; 一类是Job, 表示要干的活. 到了一定时间Trigger就会触发Job的执行, 一个Trigger只能触发一个Job, 多个Trigger都可以触发同一个Job.

剩下的就是一些辅助的东西, 有辅助Job的, 比如JobDetail, 指定一个Job实现类以及相关的信息. org.quartz.Calendar则是一些特定的时点, Trigger靠这个类精细化控制时间. 还有一个ThreadPool, 是基础设施, 执行任务都使用线程池中的线程来异步执行.

此外要了解的就是Job有一个子接口StatefulJob, 用于标记该任务是有状态任务. 类似MVC中的model一样, 无状态任务每次都有自己独立的model, 有状态model会共享一个model实例, 每一个任务执行会影响到其他任务. 有状态的没法并发执行, 无状态的可以并发执行.

如果把任务调度信息保存在数据库中, 无状态的任务仅会在注册时候持久化一次, 而有状态的每次执行完都会持久化, 也就是更新任务调度信息, 这一句暂时还不知道说的是什么, 等后边看了.

Trigger和JobDetail都需要注册到容器中, 可以放到不同的组里, 如果不指定, 都会放入默认组Scheduler.DEFAULT_GROUP中. 组名和类名组成了对象的全名, 同一类型对象的全名不相同.

SimpleTrigger

说了这么多, 来一个简单的例子, 就知道这些类的关系了. 书里的版本是1.8.6版本, 我用的是2.3.2版本, 已经有了变化, 下边的例子是根据官网的Quick-start-guide来学习的.

Quartz需要一个quartz.properties文件放到Web应用的WEB-INF/classes目录下, 以便从classpath中加载. 不过这个文件不是必需的.

先来创建一个Job的实现类, 实现Job接口和其中唯一的方法:

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("这是定时任务" + this.toString());
        System.out.println("这是任务内容");
    }
}

然后在一个类里启动Scheduler容器, 并且注册Trigger和JobDetail, 并且将二者关联起来:

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import static org.quartz.SimpleScheduleBuilder.*;

public class SimpleDemo {

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        //JobDetail的使用方式相比1.X版本有了很大的改变, 现在使用JobBuilder的静态方法, 建造者模式来创建JobDetail对象.
        //参数是之前我们创建的Job的实现类, 之后的withIdentity参数第一个是任务名, 第二个是组名
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class).withIdentity("job1", "group1").build();

        //Trigger类的写法也是使用了建造者模式, 在其中就可以设置立刻启动, 间隔时间, 重复次数等, 原来的构造器全部都删除了
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1").startNow()
                .withSchedule(simpleSchedule().withIntervalInSeconds(5).repeatForever())
                .build();

        //创建容器的方式也改成了调用静态方法
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        //在容器中注册并关联jobDetail和trigger对象
        scheduler.scheduleJob(jobDetail, trigger);

        //启动容器, 如果不调用.shutdown(), 就不会关闭, 即使没有任务也一直占用线程, 这个相比1.X版本运行结束直接关闭容器有了进步
        scheduler.start();

        Thread.sleep(30000);
        //关闭容器会直接把所有任务一起关闭
        scheduler.shutdown();
    }
}

这个程序运行的效果是每隔5秒钟, 就会运行一次MyJob方法中的任务, 通过打印对象可以看出来, 每次对象的地址都不同, 说明是一个新的对象. 这也说明了每次任务都是创建新实例, 并不复用任务对象.

所谓的SimpleTrigger, 其实现在没有这个类了, 变成了上边红字部分的simpleSchedule(). 还有一种CronTrigger, 对应的是cronSchedule

CronTrigger

把上边的例子改成cronSchedule的例子如下:

Trigger trigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
                .withSchedule(cronSchedule("0/5 * * * * ?"))
                .forJob(jobDetail)
                .build();

cronSchedule方法的参数是cron形式的字符串, 代表一个特定的时间规则. 这个时间字符串是由6个或者7个空格区分的子串组成, 详细如下:

  1. 秒, 取值0-59
  2. 分, 取值0-59
  3. 小时, 取值0-23
  4. 日, 取值1-31, 但要注意不要和月份冲突
  5. 月, 取值0-11, 也可以使用三个字符的月份简称
  6. 星期几, 取值1-7, 注意1是星期天, 7是星期六, 也可以使用字符串SUN, MON, TUE, WED, THU, FRI, SAT
  7. 年, 可以忽略

除了写具体的时点之外, 还可以使用通配符, 通配符有如下几种:

  1. *, 单独使用, 表示对应时间域的每一个时刻
  2. ?, 单独使用, 只使用在日期和星期, 表示没有意义, 也就是表示无所谓哪一天或者星期几, 类似占位符
  3. -, 需要和前后时点连用, 表示一个范围
  4. ,, 用于分割列表值中的各个值, 整个合起来表示一个列表值, 比如在星期几的位置使用MON,FRI表示星期一和星期五
  5. /, 斜杠前边表示从多少开始, 斜杠后边表示步长, 比如在分钟处使用0/5, 表示从0分开始, 每隔5分钟执行一次, 实际就会在按照0, 5, 10…..55, 0这样反复执行. 0/x也可以写成*/x
  6. L, 只在日期和星期的时候使用, 在日期中表示这个月份的最后一天. 如果用在星期中, 单独使用就表示星期六; 前边加上一个N, 就表示这个月的最后一个XXX, 比如6L表示当月的最后一个星期五
  7. W, 前边加上数字来使用, 只能用于日期, 表示离该日期最近的工作日, 但不能跨月, 默认工作日是周一到周五.
  8. LW, 单独使用, 只能用于日期, 表示当月最后一个工作日.
  9. #, 前边需要加上数字, 只能用于星期, #5表示当月的第五个工作日.
  10. C, 只在日期和星期中使用, 表示计划所关联的Calendar类代表的日期. 如果日期没有被关联, 就相当于每一天.

需要使用Cron表示方法的时候, 可以向正则一样参考一些例子, 而不用全部记在脑子里, 只要知道有这回事情就可以了.

根据表达式来看, 上边的例子就是从0秒开始, 每到5的倍数的秒执行一次.

Calendar的使用

Java提供的Calendar类是一个通用的时间类. 而Quartz的Calendar, 实际上是一个某些天的集合.

像CronTrigger, 无法配置成:每个工作日都运行, 除了五一 十一 和元旦. 这就需要补充使用Calendar, 也就是先使用Cron规则定义所有的工作日. 然后将Calendar对象设置成一个元旦, 五一和十一的集合. 之后从Cron中排除掉这三天, 就可以得到最终我们想要的规则.

Quartz 2.3 使用Calendar的方法也是在创建Trigger的时候指定. 不过我发现Quartz的官网做的实在不是很好, 新版的文档就没有很清楚写创建任务的方法, 上边的代码都是从例子中扒拉出来的. Calendar也只看到TriggerBuilder中的方法. 这一篇文章先当一个占位符, 以后有空来专门再看吧.