掐指算来, 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持久化教程,之后就准备看这两个加上官网文档了。
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个空格区分的子串组成, 详细如下:
- 秒, 取值0-59
- 分, 取值0-59
- 小时, 取值0-23
- 日, 取值1-31, 但要注意不要和月份冲突
- 月, 取值0-11, 也可以使用三个字符的月份简称
- 星期几, 取值1-7, 注意1是星期天, 7是星期六, 也可以使用字符串SUN, MON, TUE, WED, THU, FRI, SAT
- 年, 可以忽略
除了写具体的时点之外, 还可以使用通配符, 通配符有如下几种:
*
, 单独使用, 表示对应时间域的每一个时刻?
, 单独使用, 只使用在日期和星期, 表示没有意义, 也就是表示无所谓哪一天或者星期几, 类似占位符-
, 需要和前后时点连用, 表示一个范围,
, 用于分割列表值中的各个值, 整个合起来表示一个列表值, 比如在星期几的位置使用MON,FRI表示星期一和星期五/
, 斜杠前边表示从多少开始, 斜杠后边表示步长, 比如在分钟处使用0/5, 表示从0分开始, 每隔5分钟执行一次, 实际就会在按照0, 5, 10.....55, 0这样反复执行. 0/x也可以写成*/xL
, 只在日期和星期的时候使用, 在日期中表示这个月份的最后一天. 如果用在星期中, 单独使用就表示星期六; 前边加上一个N, 就表示这个月的最后一个XXX, 比如6L表示当月的最后一个星期五W
, 前边加上数字来使用, 只能用于日期, 表示离该日期最近的工作日, 但不能跨月, 默认工作日是周一到周五.LW
, 单独使用, 只能用于日期, 表示当月最后一个工作日.#
, 前边需要加上数字, 只能用于星期, #5表示当月的第五个工作日.C
, 只在日期和星期中使用, 表示计划所关联的Calendar类代表的日期. 如果日期没有被关联, 就相当于每一天.
需要使用Cron表示方法的时候, 可以向正则一样参考一些例子, 而不用全部记在脑子里, 只要知道有这回事情就可以了.
根据表达式来看, 上边的例子就是从0秒开始, 每到5的倍数的秒执行一次.
Calendar的使用
Java提供的Calendar类是一个通用的时间类. 而Quartz的Calendar, 实际上是一个某些天的集合.
像CronTrigger, 无法配置成:每个工作日都运行, 除了五一 十一 和元旦. 这就需要补充使用Calendar, 也就是先使用Cron规则定义所有的工作日. 然后将Calendar对象设置成一个元旦, 五一和十一的集合. 之后从Cron中排除掉这三天, 就可以得到最终我们想要的规则.
Quartz 2.3 使用Calendar的方法也是在创建Trigger的时候指定. 不过我发现Quartz的官网做的实在不是很好, 新版的文档就没有很清楚写创建任务的方法, 上边的代码都是从例子中扒拉出来的. Calendar也只看到TriggerBuilder中的方法. 这一篇文章先当一个占位符, 以后有空来专门再看吧.