Introduction to Quartz(1)

1 Overview
Quartz是一个开源的作业调度框架,可被集成到任何的Java EE 或者Java SE程序中。

2 Scheduler
通过SchedulerFactory接口创建Scheduler对象。当 Scheduler 实例被创建之后,就会被保存到一个仓库中(org.quartz.impl.SchedulerRepository)。Scheduler对象可以被start、standby和shutdown。一旦Scheduler被shutdown,那么不应该重新启动它。Scheduler对象被启动后,Triggers才会被触发。在Trigger被触发时,Scheduler会创建 Job 类的实例,执行实例的 execute() 方法并传入JobExecutionContext 上下文变量,JobExecutionContext封装了 Quartz 的运行时环境。

2.1 StdScheduler
可以通过调用以java.util.Properties为参数的initialize方法对StdSchedulerFactory实例进行配置。StdSchedulerFactory 还提供了一个静态方法 getDefaultScheduler()用来获得默认的Scheduler,假如之前未调用过任何一个 initialize() 方法,那么无参的 initialize() 方法会被调用。如果使用无参的 initialize 方法,StdSchedulerFactory 会执行以下几个步骤去尝试为工厂加载属性:
1. 检查System.getProperty("org.quartz.properties") 中是否设置了别的文件名;否则使用 quartz.properties 作为要加载的文件名。
2. 试图从当前工作目录中加载这个文件。
3. 试图从系统 classpath 下加载这个文件。这一步总是能成功的,因为在 Quartz Jar 包中有一个默认的 quartz.properties 文件。

以下是个quartz.properties 文件的例子:

#===============================================================
#Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

#===============================================================
#Configure ThreadPool
#===============================================================
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

#===============================================================
#Configure JobStore
#===============================================================
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.jobStore.misfireThreshold = 60000

org.quartz.scheduler.instanceName用于指定Scheduler的实例名,可以是任意字符串。org.quartz.scheduler.instanceId用于指定实例ID,也可以是任意字符串。在集群环境中instanceId必须唯一。如果希望 Quartz 自动生成这个值,那么可以设置为 AUTO。如果 Quartz 框架是运行在非集群环境中,那么自动产生的值将会是 NON_CLUSTERED;如果是在集群环境下使用 Quartz,这个值将会是主机名加上当前的日期和时间。
org.quartz.threadPool.class属性值是一个实现了 org.quartz.spi.ThreadPool 接口的全限定类名。Quartz 自带的实现类是 org.quartz.smpl.SimpleThreadPool,它提供了固定大小的线程池,并经很好的测试过,能满足绝大多数情况下的需求。你也可以指定其它的线程池实现,例如需要一个可伸缩的线程池。
org.quartz.jobStore.class属性指定了在Scheduler的生命周期中,Job 和 Trigger 信息是如何被存储的。如果使用RAMJobStore,那么一旦调度器进程被终止,所有的 Job 和 Trigger 的状态就丢失了。除了RAMJobStore,还可以指定JDBCJobStore。虽然JDBCJobStore的性能不如RAMJobStore,但是可以持久地保存Job 和 Trigger 信息。
以下是个简单的例子:

public class DummyJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(sdf.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
JobDetail jobDetail = new JobDetail("myJob", null, DummyJob.class);
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jobDetail, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}


3 Job
一个 Quartz Job 就是一个执行任务的 Java 类。Job 的实例要到该执行它们的时候才会实例化出来。这意味着 Job 不必担心线程安全性,因为同一时刻仅有一个线程去执行给定 Job 类的实例。Job有以下三个主要的属性:
易失性(volatility)。Job在程序关闭之后是否被持久化。 通过调用 JobDetail 的 setVolatility(boolean flag) 进行设置,默认值是 false。RamJobStore 使用的是非永久性存储器,所有关于 Job 和 Trigger 的信息会在程序关闭之后丢失。保存 Job 到 RamJobStore使得它们是易失性的。假如你需要让你的 Job 信息在程序重启之后仍能保留下来,你就该使用JDBCJobStore。
持久性(Durability)。 在所有的触发器触发之后,是否将Job从 JobStore 中移除。通过调用 JobDetail 的 setDurability(boolean flag) 方法设置,默认值是 false。假设设置了一个单次触发的 Trigger,触发之后它就变成了 STATE_COMPLETE 状态。这个 Trigger 指向的 Job 现在成了一个孤儿 Job,因为不再有任何 Trigger 与之相关联了。假如你设置这个Job为持久的,那么即使它成了孤儿 Job 也不会从 JobStore 移除掉。这样可以保证在将来,无论何时你的程序决定为这个 Job 增加另一个 Trigger 都是可用的。
可恢复性(Recovery)。当一个 Job 还在执行中,Scheduler 经历了一次非预期的关闭,在 Scheduler 重启之后该 Job 是否会重头开始再次被执行。通过调用 JobDetail 的 setRequestsRecovery(boolean flag)方法设置,默认值是 false。

3.1 JobDetail
实际上,并不是直接把 Job 对象注册到 Scheduler,而是注册一个 JobDetail 实例。JobDetail用于对Job进行配置。每当Trigger被触发时,Scheduler会创建相对应的Job的实例并调用其execute方法。传入的JobExecutionContext参数包含了该Job的运行时环境,例如得到相关的Scheduler、Trigger和JobDetail。

3.2 JobDataMap
由于每次调度Job的时候都有一个新的Job实例被创建,因此每次Job被调度后状态都丢失了。通过使用JobDataMap,可以向Job传递配置信息,或者保存多次调度之间的作业状态。JobDataMap 通过它的超类 org.quartz.util.DirtyFlagMap 实现了java.util.Map 接口。如果你使用的是持久化的JobStore(例如JDBCJobStore),那么JobDataMap中存放的数据必须能够被序列化。
可以在JobDetail和Trigger上设置JobDataMap。如果向 JobDataMap 中存入键/值对,那么这些数据可以在Job实例被执行时,通过传入参数JobExecutionContext中包含的Trigger和JobDetail实例访问到,因此这是一个向 Job实例传送配置的信息便捷方法。JobExecutionContext提供了一个方法getMergedJobDataMap,用于得到JobDetail和Trigger中包含的JobDataMap的合并后版本。需要注意的是,如果键/值对冲突,那么Trigger中包含的JobDataMap的值会覆盖掉JobDetail中包含的JobDataMap的值。以下是个简单的例子:

public class DummyJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String name = context.getMergedJobDataMap().getString("name");
System.out.println(df.format(new Date()) + " Job executed, name: " + name);
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jobDetail = new JobDetail("myJob", null, DummyJob.class);
jobDetail.getJobDataMap().put("name", "job1");
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jobDetail, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}


3.3 Job
Job定义如下:

public interface Job {
execute(JobExecutionContext context) throws JobExecutionException;
}

每当Scheduler调度Job的时候,Scheduler根据配置的JobFactory创建一个新的Job实例。默认的JobFactory(org.quartz.simpl.SimpleJobFactory)只是通过反射,即调用Job类的newInstance方法构造Job实例,因此Job类需要有个可访问的无参构造函数。

3.4 StatefulJob
StatefulJob扩展Job,没有添加新的方法,其定义如下:

public interface StatefulJob extends Job {
}

当你需要在两次 Job 执行间维护状态的话,Quartz 框架为此提供了 org.quartz.StatefulJob 接口。Job 和 StatefulJob 存在以下两个关键差异:
JobDataMap 在每次执行之后重新持久化到 JobStore 中。这样就确保对stateful Job执行时对JobDataMap的修改都会保存,下次执行时也仍然可见。对于non-stateful Job,JobDataMap只是在JobDetail被注册到Scheduler的时候持久化到JobStore 中,这意味着每次non-stateful Job执行时对JobDataMap的修改都会被丢弃。
两个或多个有状态的 JobDetail 实例不能并发执行。如果有两个Trigger试图同时触发一个相同的stateful Job,那么其中一个Trigger会被阻塞,直到另外一个Trigger触发的Job执行完毕。这确保了不会对JobDataMap进行并发的修改,从而确保了线程安全。
下面是个使用StatefulJob接口的例子:

public class DummyStatefulJob implements StatefulJob {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
JobDataMap map = context.getJobDetail().getJobDataMap();
Integer round = (Integer)map.get("round");
if(round == null) {
round = new Integer(1);
} else {
round = new Integer(round.intValue() + 1);
}
map.put("round", round);
System.out.println(df.format(new Date()) + " Job executed, round: " + round);
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jd = new JobDetail("myJob", null, DummyStatefulJob.class);
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jd, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}

以上程序的输出如下:

2013-04-26 11:20:50.357 Job executed, round: 1
2013-04-26 11:20:52.357 Job executed, round: 2
2013-04-26 11:20:54.342 Job executed, round: 3
2013-04-26 11:20:56.342 Job executed, round: 4
2013-04-26 11:20:58.342 Job executed, round: 5
2013-04-26 11:21:00.342 Job executed, round: 6


3.5 InterruptableJob
InterruptableJob扩展Job,其定义如下:

public interface InterruptableJob extends Job {
void interrupt() throws UnableToInterruptJobException;
}

可以通过调用Scheduler 接口的interrupt(String jobName, String groupName) 方法来中断特定的Job。Scheduler会负责调用InterruptableJob的interrupt()方法。至于Job如何被中断,这要取决于Job的具体实现。下面是个使用InterruptableJob接口的例子:

public class DummyInterruptableJob implements InterruptableJob {
//
private final AtomicBoolean interrupted = new AtomicBoolean(false);


@Override
public void interrupt()
throws UnableToInterruptJobException {
interrupted.compareAndSet(false, true);
}

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
while(!interrupted.get()) {
System.out.println(df.format(new Date()) + " Job executing");
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
System.out.println(df.format(new Date()) + " Job interrupted");
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jd = new JobDetail("job1", null, DummyInterruptableJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", null);
trigger.setRepeatCount(0);
scheduler.scheduleJob(jd, trigger);
Thread.sleep(5000);
scheduler.interrupt("job1", null);
scheduler.shutdown();
}
}

以上程序的输出如下:

2010-08-09 08:58:54.248 Job executing
2010-08-09 08:58:55.248 Job executing
2010-08-09 08:58:56.248 Job executing
2010-08-09 08:58:57.248 Job executing
2010-08-09 08:58:58.248 Job executing
2010-08-09 08:58:59.248 Job interrupted

需要注意的是,以上例子中构造JobDetail的第二个参数group为null(即使用默认的group),因此在调用Scheduler的interrupt()方法时的第二个参数(group)也必须为null。

4 Trigger
Job 包含了要执行任务的逻辑,Trigger 决定了Job 何时被执行。可以为单个Job 使用多个 Trigger,但一个 Trigger 只能被指派给一个 Job。Trigger上也可以配置JobDataMap,当Trigger触发时,被执行的Job实例可以获得Trigger所关联的JobDataMap。特别是当多个Trigger被关联到一个JobDetail时,可以通过Trigger所关联的JobDataMap向Job提供额外的配置信息。Quartz提供了多种Trigger实现,最常用的有SimpleTrigger、NthIncludeDayTrigger和CronTrigger。
某一时刻,假设有m个trigger可能被触发,但是只有n(m > n)个工作线程可用,那么具有最高优先级的n个trigger会优先被触发。通过Trigger的setPriority(int priority)方法可以调整Trigger的优先级,默认优先级是5。
Trigger的另外一个比较重要的属性是misfire instruction。由于scheduler被shutdown,trigger被pause或者没有可用的工作线程等原因,都会导致trigger没有被触发。不同类型的Trigger可以设置不同的misfire instruction。

4.1 SimpleTrigger
SimpleTrigger是最简单的一种 Trigger。 如果你需要某个Job在特定时间执行一次,或者在某个时间执行一次后再以特定的间隔重复执行 n 次,那么可以使用SimpleTrigger。SimpleTrigger包含以下属性:开始时间、结束时间、重复次数和重复间隔。重复次数可以是0、正整数或者SimpleTrigger.REPEAT_INDEFINITELY。重复间隔的单位是毫秒。如果设置了结束时间,那么它会覆盖重复次数,例如你设置了开始时间、结束时间和重复间隔,那么Quartz会自动计算重复次数。下面是个使用SimpleTrigger的例子:

public class SimpleTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String startedTime = df.format(new Date());
try {
System.out.println("Job started, started time: " + startedTime);
Thread.sleep(3000);
} catch(Exception e) {
throw new JobExecutionException(e);
} finally {
System.out.println("Job stopped, started time: " + startedTime);
}
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "5");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", SimpleTriggerJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(2);
trigger.setRepeatInterval(1000);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上程序的输出如下:

Job started, started time: 2013-04-26 09:29:37.765
Job started, started time: 2013-04-26 09:29:38.750
Job started, started time: 2013-04-26 09:29:39.750
Job stopped, started time: 2013-04-26 09:29:37.765
Job stopped, started time: 2013-04-26 09:29:38.750
Job stopped, started time: 2013-04-26 09:29:39.750

如果将以上例子中props的org.quartz.threadPool.threadCount属性设置为2(也就是说最多同时能执行2个Job),那么程序的输出如下:

Job started, started time: 2013-04-26 09:31:10.156
Job started, started time: 2013-04-26 09:31:11.125
Job stopped, started time: 2013-04-26 09:31:10.156
Job started, started time: 2013-04-26 09:31:13.156
Job stopped, started time: 2013-04-26 09:31:11.125
Job stopped, started time: 2013-04-26 09:31:13.156

从以上输出可以看出,本应该在09:31:12左右调度的第三个任务,由于没有可用的线程而被阻塞,直到第一个任务结束之后(09:31:13)才被调度。需要注意的是,如果给trigger设置不同的misfire instruction,可能会有不同的行为。

4.2 NthIncludeDayTrigger
NthIncludedDayTrigger用于在每一间隔类型的第几天执行 Job,例如要在每个月的 15 号执行特定的 Job。目前Quartz 支持的间隔类型有:INTERVAL_TYPE_WEEKLY、INTERVAL_TYPE_MONTHLY和INTERVAL_TYPE_YEARLY。下面是个使用NthIncludeDayTrigger的例子:

public class NthIncludedDayTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
JobDataMap map = context.getJobDetail().getJobDataMap();
String name = map.getString("name");
System.out.println(df.format(new Date()) + " Job executed, name: " + name);
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", NthIncludedDayTriggerJob.class);
NthIncludedDayTrigger trigger = new NthIncludedDayTrigger("trigger1", "group1");
trigger.setN(15);
trigger.setIntervalType(NthIncludedDayTrigger.INTERVAL_TYPE_MONTHLY);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,任务被配置成在每月15日执行。执行时间可以通过NthIncludedDayTrigger的setFireAtTime(String fireAtTime)方法设置,默认是12:00:00。

4.3 CronTrigger
SimpleTrigger 对于需要在指定的时间(毫秒级)执行的Job还是不错的,但是假如你的Job需要更复杂的执行计划时,可能需要使用CronTrigger 提供的更灵活的功能。
CronExpression用于配置CronTrigger,它封装了一个字符串,这个字符串由空格分割的7个子表达式构成,它们分别代表:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (可选)

例如"0 0 12 ? * WED"代表每周三的12点,"* * * ? * *"则代表每秒都执行。每个子表达式都有可选值的集合:Seconds和Minutes的可选值是0到59;Hours的可选值是0到23;Day-of-Month的可选值是1到31;Months的可选值是0到11,或者使用JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV和DEC;Days-of-Week的可选值是1到7(1代表周日),或者是SUN、MON、TUE、WED、THU、FRI和SAT。每个子表达式也可以包含范围和列表。例如"MON-FRI"代表一个范围,即周一到周五;"MON, WED, FRI"代表一个列表,即周一、周三和周五;"MON-WED,SAT"代表周一、周二、周三和周六。
CronExpression表达式支持用特殊字符来创建更为复杂的执行计划。下面是可以在CronExpression中使用的特殊字符:
通配符"*"代表每一个可能的值。
"/"用于指定值的增量,例如Minutes字段上的"0/15"代表从0分钟起每隔15分钟。
"?"可以放在Day-of-Month和Day-of-Week中。"?"表示这个字段不包含具体值。所以如果指定Day-of-Month,那么可以在Day-of-Week字段中设置"?",表示Day-of-Week值无关紧要。
"L"可以放在Day-of-Month和Day-of-Week中。Day-of-Month表示在当月最后一天执行。在Day-of-Week中,如果"L"单独存在,就等于7(星期六),否则代表当月内或者周内日期的最后一个实例。例如"* * * L 1 ? 2008"匹配2008-01-31(星期四);"* * * ? 1 L 2008"匹配2008-01-05(星期六)、2008-01-12(星期六)、2008-01-19(星期六)和2008-01-26(星期六)。
"W"可以放在Day-of-Month中。W 字符代表着工作日 (周一到周五),用来指定离指定日的最近的一个平日。例如,Day-of-Month中的 15W 意味着 "离该月15日的最近一个工作日"。如果15日是工作日,那么就在15日执行; 假如15日是星期六,那么会在14日(周五)执行;如果15日是周日,那么会在16日(周一)执行。
"#" 字符仅能用于Day-of-Week中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。
下面是个关于CronExpression的例子程序:

public class CronExpressionTest {

public static void main(String args[]) throws Exception {
//
String expression[] = new String[]{
"* * * 4W 1 ? 2008",
"* * * 5W 1 ? 2008",
"* * * 6W 1 ? 2008",
"* * * 7W 1 ? 2008",
"* * * LW 1 ? 2008",
"* * * L 1 ? 2008",
"* * * ? 1 L 2008",
"* * * ? 1 0L 2008",
"* * * ? 1 1L 2008",
"* * * ? 1 2L 2008",
"* * * ? 1 3L 2008",
"* * * ? 1 4L 2008",
"* * * ? 1 5L 2008",
"* * * ? 1 6L 2008",
"* * * ? 1 7L 2008",
"* * * ? 1 1#1 2008",
"* * * ? 1 1#2 2008",
"* * * ? 1 1#3 2008",
"* * * ? 1 1#4 2008",
"* * * ? 1 1#5 2008"
};

//
for(int i = 0; i < expression.length; i++) {
System.out.println("######################");
CronExpression cp = new CronExpression(expression[i]);
for(int j = 0; j <= 31; j++) {
Date d = getDate(2008, 1, j);
if(cp.isSatisfiedBy(d)) {
System.out.println(cp.getCronExpression() + " matches " + print(d));
}
}
}
}

private static String print(Date date) {
SimpleDateFormat d = new SimpleDateFormat("yyyy-MM-dd(E)");
return d.format(date);
}

private static Date getDate(int year, int month, int dayOfMonth) {
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month - 1);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
return c.getTime();
}
}

以上程序输出如下:

######################
* * * 4W 1 ? 2008 matches 2008-01-04(星期五)
######################
* * * 5W 1 ? 2008 matches 2008-01-04(星期五)
######################
* * * 6W 1 ? 2008 matches 2008-01-07(星期一)
######################
* * * 7W 1 ? 2008 matches 2008-01-07(星期一)
######################
* * * LW 1 ? 2008 matches 2008-01-31(星期四)
######################
* * * L 1 ? 2008 matches 2008-01-31(星期四)
######################
* * * ? 1 L 2008 matches 2008-01-05(星期六)
* * * ? 1 L 2008 matches 2008-01-12(星期六)
* * * ? 1 L 2008 matches 2008-01-19(星期六)
* * * ? 1 L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 0L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 1L 2008 matches 2008-01-27(星期日)
######################
* * * ? 1 2L 2008 matches 2008-01-28(星期一)
######################
* * * ? 1 3L 2008 matches 2008-01-29(星期二)
######################
* * * ? 1 4L 2008 matches 2008-01-30(星期三)
######################
* * * ? 1 5L 2008 matches 2008-01-31(星期四)
######################
* * * ? 1 6L 2008 matches 2008-01-25(星期五)
######################
* * * ? 1 7L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 1#1 2008 matches 2008-01-06(星期日)
######################
* * * ? 1 1#2 2008 matches 2008-01-13(星期日)
######################
* * * ? 1 1#3 2008 matches 2008-01-20(星期日)
######################
* * * ? 1 1#4 2008 matches 2008-01-27(星期日)
######################

下面是个使用CronTrigger的例子:

public class CronTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "2");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
scheduler.start();

//
Date startTime = new Date();
Date endTime = new Date(startTime.getTime() + 20 * 1000);

//
JobDetail jd = new JobDetail("job1", "group1", CronTriggerJob.class);
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setStartTime(startTime);
trigger.setEndTime(endTime);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

在以上例子中,指定从每分钟的0秒起,每个5秒调度一次。此外还使用 CronTrigger 的 setStartTime方法 和 setEndTime方法限制了执行的时间范围。
CronTrigger支持的misfire instruction有MISFIRE_INSTRUCTION_SMART_POLICY(0),MISFIRE_INSTRUCTION_FIRE_ONCE_NOW(1)和MISFIRE_INSTRUCTION_DO_NOTHING(2)。其中MISFIRE_INSTRUCTION_SMART_POLICY等效于MISFIRE_INSTRUCTION_FIRE_ONCE_NOW。以下是关于misfire instruction的简单例子:

public class SimpleTriggerListener implements TriggerListener {
//
private String name;
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

public SimpleTriggerListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void triggerFired(Trigger trigger, JobExecutionContext context) {
System.out.println(dateFormat.format(new Date()) + " triggerFired, trigger name: " + trigger.getName());
}

public void triggerMisfired(Trigger trigger) {
System.out.println(dateFormat.format(new Date()) + " triggerMisfired, trigger name: " + trigger.getName());
}

public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode) {
System.out.println(dateFormat.format(new Date()) + " triggerComplete, trigger name: " + trigger.getName());
}

public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
return false;
}
}

public class CronTriggerJob implements StatefulJob {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Integer round = (Integer)context.getJobDetail().getJobDataMap().get("round");
if(round == null) {
round = new Integer(1);
} else {
round = new Integer(round.intValue() + 1);
}
context.getJobDetail().getJobDataMap().put("round", round);

System.out.println(df.format(new Date()) + " Job executed, round: " + round);
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "5");
props.put("org.quartz.jobStore.misfireThreshold", "1000");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
SimpleTriggerListener stl = new SimpleTriggerListener("triggerListener1");
scheduler.addTriggerListener(stl);
scheduler.start();

//
Date startTime = new Date();
Date endTime = new Date(startTime.getTime() + 6 * 1000);

//
JobDetail jd = new JobDetail("job1", "group1", CronTriggerJob.class);
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/1 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setStartTime(startTime);
trigger.setEndTime(endTime);
trigger.addTriggerListener(stl.getName());
trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_SMART_POLICY);
scheduler.scheduleJob(jd, trigger);

//
Thread.sleep(2000);
System.out.println("start to pause trigger");
scheduler.pauseTrigger("trigger1", "group1");
Thread.sleep(2500);
System.out.println("start to resume trigger");
scheduler.resumeTrigger("trigger1", "group1");
Thread.sleep(2000);
System.out.println("start to shutdown scheduler");
scheduler.shutdown();
}
}

以上例子中,如果将trigger的miscfire instruction设置为MISFIRE_INSTRUCTION_DO_NOTHING,那么输出可能如下:

2010-08-09 14:34:50.890 triggerFired, trigger name: trigger1
2010-08-09 14:34:50.890 Job executed, round: 1
2010-08-09 14:34:50.890 triggerComplete, trigger name: trigger1
2010-08-09 14:34:51.015 triggerFired, trigger name: trigger1
2010-08-09 14:34:51.015 Job executed, round: 2
2010-08-09 14:34:51.015 triggerComplete, trigger name: trigger1
2010-08-09 14:34:52.015 triggerFired, trigger name: trigger1
2010-08-09 14:34:52.015 Job executed, round: 3
2010-08-09 14:34:52.015 triggerComplete, trigger name: trigger1
start to pause trigger
start to resume trigger
2010-08-09 14:34:55.390 triggerMisfired, trigger name: trigger1
2010-08-09 14:34:56.000 triggerFired, trigger name: trigger1
2010-08-09 14:34:56.000 Job executed, round: 4
2010-08-09 14:34:56.000 triggerComplete, trigger name: trigger1
start to shutdown scheduler
如果将trigger的miscfire instruction设置为MISFIRE_INSTRUCTION_SMART_POLICY或者MISFIRE_INSTRUCTION_FIRE_ONCE_NOW,那么输出可能如下:
2010-08-09 14:35:20.921 triggerFired, trigger name: trigger1
2010-08-09 14:35:20.921 Job executed, round: 1
2010-08-09 14:35:20.921 triggerComplete, trigger name: trigger1
2010-08-09 14:35:21.015 triggerFired, trigger name: trigger1
2010-08-09 14:35:21.015 Job executed, round: 2
2010-08-09 14:35:21.015 triggerComplete, trigger name: trigger1
2010-08-09 14:35:22.015 triggerFired, trigger name: trigger1
2010-08-09 14:35:22.015 Job executed, round: 3
2010-08-09 14:35:22.015 triggerComplete, trigger name: trigger1
start to pause trigger
start to resume trigger
2010-08-09 14:35:25.421 triggerMisfired, trigger name: trigger1
2010-08-09 14:35:25.421 triggerFired, trigger name: trigger1
2010-08-09 14:35:25.421 Job executed, round: 4
2010-08-09 14:35:25.421 triggerComplete, trigger name: trigger1
2010-08-09 14:35:26.000 triggerFired, trigger name: trigger1
2010-08-09 14:35:26.000 Job executed, round: 5
2010-08-09 14:35:26.000 triggerComplete, trigger name: trigger1
start to shutdown scheduler


5 Calendar
跟 java.util.Calendar不同,Quartz Calendar用于指定一个时间区间,使 Trigger 在这个区间中不被触发。org.quartz.Calendar 接口中两个最重要的方法如下:
public long getNextIncludedTime(long timeStamp);
public boolean isTimeIncluded(long timeStamp);
Calendar 接口方法参数的类型是 long,这说明 Quartz Calender 能够排除的时间可以精确到毫秒级。要使用 Quartz Calendar,首先要把Calendar实例添加到Scheduler 中,然后在 Trigger 实例中指定关联的Calendar名字。

5.1 Builtin Calendars
Quartz 提供了以下常用的 Calender:
BaseCalender。为其它的Calender 实现了基本的功能。
WeeklyCalendar。 排除星期中的一天或多天。
MonthlyCalendar。 排除月的数天。
AnnualCalendar。排除年中一天或多天。
HolidayCalendar。特别用于排除特定节假日
下面是个使用AnnualCalendar的例子:

public class AnnualCalendarJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", AnnualCalendarJob.class);

//
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, Calendar.JANUARY);
c.set(Calendar.DATE, 1);

AnnualCalendar ac = new AnnualCalendar();
ac.setDayExcluded(c, true);
scheduler.addCalendar("calendar1", ac, true, true);

//
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setCalendarName("calendar1");
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,任务在每年的一月一日都不会被调度。

5.2 Customized Calendar
除了使用Quartz提供的Calendar,我们也可以创建定制的Calendar,例如创建一个 HourlyCalendar用于排除小时中的特定分钟。

public class HourlyCalendar extends BaseCalendar {
//
private static final long serialVersionUID = -4438834879438520769L;

//
private CopyOnWriteArrayList<Integer> excludedMinutes = new CopyOnWriteArrayList<Integer>();

/**
*
*/
public HourlyCalendar() {
super();
}

public HourlyCalendar(Calendar baseCalendar) {
super(baseCalendar);
}

/**
*
*/
public boolean isTimeIncluded(long msec) {
//
if (super.isTimeIncluded(msec) == false) {
return false;
}

//
final java.util.Calendar cal = createJavaCalendar(msec);
int minute = cal.get(java.util.Calendar.MINUTE);
return !(isMinuteExcluded(minute));
}

public long getNextIncludedTime(long msec) {
//
long baseTime = super.getNextIncludedTime(msec);
if ((baseTime > 0) && (baseTime > msec)) {
msec = baseTime;
}

//
final java.util.Calendar cal = getStartOfSecond(msec);
int minute = cal.get(java.util.Calendar.MINUTE);
if (isMinuteExcluded(minute) == false) {
return msec;
}

//
while (isMinuteExcluded(minute) == true) {
cal.add(java.util.Calendar.MINUTE, 1);
minute = cal.get(java.util.Calendar.MINUTE);
}
return cal.getTime().getTime();
}

public List<Integer> getMinutesExcluded() {
return excludedMinutes;
}

public void setMinuteExcluded(int minute) {
if (isMinuteExcluded(minute)) {
return;
}
excludedMinutes.add(new Integer(minute));
}

public void setMinutesExcluded(List<Integer> minutes) {
if (minutes == null) {
this.excludedMinutes.clear();
} else {
this.excludedMinutes.addAll(minutes);
}
}

public boolean isMinuteExcluded(int minute) {
for(Integer excludedMinute : this.excludedMinutes) {
if (minute == excludedMinute.intValue()) {
return true;
}
}
return false;
}

/**
*
*/
private java.util.Calendar getStartOfSecond(long msec) {
final java.util.Calendar c = (super.getTimeZone() == null) ? java.util.Calendar.getInstance() : java.util.Calendar.getInstance(getTimeZone());
c.setTimeInMillis(msec);
c.set(java.util.Calendar.SECOND, 0);
c.set(java.util.Calendar.MILLISECOND, 0);
return c;
}
}

public class HourlyCalendarJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", HourlyCalendarJob.class);

//
HourlyCalendar cal = new HourlyCalendar();
for (int i = 0; i < 60; i++) {
if (i % 2 == 0) {
cal.setMinuteExcluded(i);
}
}

//
long now = System.currentTimeMillis();
for(int i = 0; i < 100; i++) {
long next = cal.getNextIncludedTime(now);
System.out.println(new Date(next));
}
scheduler.addCalendar("hourlyCalendar1", cal, true, true);

//
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setCalendarName("hourlyCalendar1");
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String line = br.readLine();
if (line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,只有奇数分钟内,任务才会被调度。

6 Listener
6.1 JobListener
JobListener 的定义如下:

public interface JobListener{
String getName();
void jobToBeExecuted(JobExecutionContext context);
void jobExecutionVetoed(JobExecutionContext context);
void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}

getName() 方法返回一个字符串用以说明 JobListener 的名称。
Scheduler 在 JobDetail 将要被执行时调用jobToBeExecuted()方法。
Scheduler 在 JobDetail 即将被执行,却又被 TriggerListener 否决了时调用jobExecutionVetoed()方法。
Scheduler 在 JobDetail 被执行之后调用jobWasExecuted()方法。
下面是个关于JobListener的例子:

public class SimpleJobListener implements JobListener {
//
private String name;

public SimpleJobListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("jobToBeExecuted, job name: " + context.getJobDetail().getName());
}

public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println("jobExecutionVetoed, job name: " + context.getJobDetail().getName());
}

public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println("jobWasExecuted, job name: " + context.getJobDetail().getName());
}
}

public class ListenerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

SimpleJobListener sjl = new SimpleJobListener("listener1");
scheduler.addJobListener(sjl);

JobDetail jd = new JobDetail("job1", "group1", ListenerJob.class);
jd.addJobListener(sjl.getName());

SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(3);
trigger.setRepeatInterval(1000);
scheduler.scheduleJob(jd, trigger);

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,SimpleJobListener是作为一个非全局的JobListener注册到Scheduler的。首先调用Scheduler的addJobListener()方法,方法的参数是JobListener实例。然后对于任何引用到它的JobDetail,调用addJobListener()方法,方法的参数是JobListener的名称(也就是JobListener的getName()方法返回的名称)。如果需要注册一个全局的JobListener,只需要调用Scheduler的addGlobalJobListener()方法即可,这个JobListener就会和所有Job相关联。

6.2 TriggerListener
TriggerListener接口的定义如下:

public interface TriggerListener {
String getName();
void triggerFired(Trigger trigger, JobExecutionContext context);
boolean vetoJobExecution(Trigger trigger, JobExecutidonContext context);
void triggerMisfired(Trigger trigger);
void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode);
}

和前面提到的 JobListener 一样,TriggerListner 接口的 getName() 返回一个字符串用以说明监听器的名称。
当与监听器相关联的 Trigger 被触发,Job 上的 execute() 方法将要被执行时,Scheduler调用triggerFired()方法。
Scheduler在 Trigger 触发后,Job 将要被执行时调用vetoJobExecution()方法。vetoJobExecution()方法给了TriggerListener 否决 Job 的执行的权力,如果返回 true,那么这个 Job 将不会为此次 Trigger的触发而得到执行。
Scheduler在 Trigger 错过触发时调用triggerMisfired() 方法。
Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用triggerComplete()方法。
以下是个关于TriggerListener的例子:

public class SimpleTriggerListener implements TriggerListener {
//
private String name;

public SimpleTriggerListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void triggerFired(Trigger trigger, JobExecutionContext context) {
System.out.println("triggerFired, trigger name: " + trigger.getName());
}

public void triggerMisfired(Trigger trigger) {
System.out.println("triggerMisfired, trigger name: " + trigger.getName());
}

public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode) {
System.out.println("triggerComplete, trigger name: " + trigger.getName());
}

public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
boolean r = (System.currentTimeMillis() / 1000) % 2 == 0;
System.out.println("vetoJobExecution, trigger name: " + trigger.getName() + ", voted: " + r);
return r;
}
}

public class ListenerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

SimpleJobListener sjl = new SimpleJobListener("jobListener1");
scheduler.addJobListener(sjl);

SimpleTriggerListener stl = new SimpleTriggerListener("triggerListener1");
scheduler.addTriggerListener(stl);

JobDetail jd = new JobDetail("job1", "group1", ListenerJob.class);
jd.addJobListener(sjl.getName());

SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(100);
trigger.setRepeatInterval(500);
trigger.addTriggerListener(stl.getName());
scheduler.scheduleJob(jd, trigger);

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,SimpleTriggerListener是作为一个非全局的TriggerListener注册到Scheduler的。首先调用Scheduler的addTriggerListener()方法,方法的参数是TriggerListener实例。然后对于任何引用到它的Trigger,调用addTriggerListener ()方法,方法的参数是TriggerListener的名称(也就是TriggerListener的getName()方法返回的名称)。如果需要注册一个全局的TriggerListener,只需要调用Scheduler的addGlobalTriggerListener()方法即可,这个TriggerListener就会和所有Trigger相关联。以上程序的输出如下:

triggerFired, trigger name: trigger1
vetoJobExecution, trigger name: trigger1, voted: false
jobToBeExecuted, job name: job1
2010-08-09 11:39:25.546 Job executed
jobWasExecuted, job name: job1
triggerComplete, trigger name: trigger1
triggerFired, trigger name: trigger1
vetoJobExecution, trigger name: trigger1, voted: true
jobExecutionVetoed, job name: job1

以上例子中,在偶数秒内调度的任务都被否决了。需要注意的是,如果TriggerListener中否决了某个任务,那么相应的triggerComplete()方法不会被调用。

6.3 SchedulerListener
SchedulerListener 接口包含了一系列的回调方法,它们会在 Scheduler 的生命周期中有关键事件发生时被调用。

public interface SchedulerListener {
void jobScheduled(Trigger trigger);
void jobUnscheduled(String triggerName, String triggerGroup);
void triggerFinalized(Trigger trigger);
void triggersPaused(String triggerName, String triggerGroup);
void triggersResumed(String triggerName,String triggerGroup);
void jobsPaused(String jobName, String jobGroup);
void jobsResumed(String jobName, String jobGroup);
void schedulerError(String msg, SchedulerException cause);
void schedulerShutdown();
}

Scheduler 在有新的 JobDetail 部署或卸载时调用jobScheduled() 和 jobUnscheduled()方法。
当一个 Trigger 到了再也不会触发的状态时调用triggerFinalized() 方法,除非与之关联 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。
Scheduler 在发生在一个 Trigger 或 Trigger 组被暂停时调用triggersPaused() 方法。如果是 Trigger 组,那么triggerName 参数将为 null。
Scheduler 在发生成一个 Trigger 或 Trigger 组从暂停中恢复时调用triggersResumed() 方法。如果是 Trigger 组,那么triggerName 参数将为 null。
当一个或一组 JobDetail 暂停时调用jobsPaused() 方法方法。
当一个或一组 Job 从暂停上恢复时调用jobsResumed() 方法。如果是一个 Job 组,那么jobName 参数将为 null。
在 Scheduler 的正常运行期间产生一个严重错误时调用schedulerError() 方法。你可以使用 SchedulerException 的 getErrorCode() 或者 getUnderlyingException() 方法或获取到特定错误的更详尽的信息。
Scheduler 调用schedulerShutdown() 方法用来通知 SchedulerListener Scheduler 将要被关闭。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园建设方案旨在通过融合先进技术,如物联网、大数据、人工智能等,实现校园的智能化管理与服务。政策的推动和技术的成熟为智慧校园的发展提供了基础。该方案强调了数据的重要性,提出通过数据的整合、开放和共享,构建产学研资用联动的服务体系,以促进校园的精细化治理。 智慧校园的核心建设任务包括数据标准体系和应用标准体系的建设,以及信息化安全与等级保护的实施。方案提出了一站式服务大厅和移动校园的概念,通过整合校内外资源,实现资源共享平台和产教融合就业平台的建设。此外,校园大脑的构建是实现智慧校园的关键,它涉及到数据中心化、数据资产化和数据业务化,以数据驱动业务自动化和智能化。 技术应用方面,方案提出了物联网平台、5G网络、人工智能平台等新技术的融合应用,以打造多场景融合的智慧校园大脑。这包括智慧教室、智慧实验室、智慧图书馆、智慧党建等多领域的智能化应用,旨在提升教学、科研、管理和服务的效率和质量。 在实施层面,智慧校园建设需要统筹规划和分步实施,确保项目的可行性和有效性。方案提出了主题梳理、场景梳理和数据梳理的方法,以及现有技术支持和项目分级的考虑,以指导智慧校园的建设。 最后,智慧校园建设的成功依赖于开放、协同和融合的组织建设。通过战略咨询、分步实施、生态建设和短板补充,可以构建符合学校特色的生态链,实现智慧校园的长远发展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值