Java定时任务利器:Quartz实战指南
2025-06-26
Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。
核心概念
Quartz框架中的三个核心概念分别是Scheduler(调度器)、Trigger(触发器)和Job(任务)。
Scheduler:谁来做?怎么做?
Scheduler在Quartz中主要负责控制任务的执行,并在任务执行时为其附加设定的数据。在单机程序上,它会选择最合适的线程来执行任务。在分布式应用中,我们可以自定义Scheduler,使其它能通过数据库存储任务数据,并通过为任务加锁的方式来保证任务仅会被执行一次。
Trigger:什么时候做?
我们的定时任务可能是需要延迟几秒启动,也可能是在某个时间段内重复执行,或者是根据cron表达式来定制在每天的某个时间执行一次,所有的这些内容都可以通过Trigger来定制。
Job:做什么?
创建一个新的类,并使其实现Job接口,然后在execute方法中定制我们任务的具体内容。
要注意这三者之间没有特定的依赖关系,开发者可以根据自己的需求自由组装它们。
创建一个定时任务
了解Quartz的核心概念之后,我们可以通过一个例子来快速上手这个框架。
先从一个最简单的例子开始:每秒钟在控制台输出一次当前的时间。
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever())
.build()
);
scheduler.start();
}
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println(DATE_FORMAT.format(new Date()));
}
}
}
在这段代码中,我们首先使用StdSchedulerFactory.getDefaultScheduler();
创建了一个调度器,然后通过scheduler.scheduleJob()
方法为这个调度器注册了任务。
我们新建了一个MyJob
,使其作为Job
接口的实现,并完成了我们的业务内容。
在触发器这一部分,我们使用SimpleScheduleBuilder.repeatSecondlyForever()
来指定这个触发器每秒执行一次。
最后,我们使用scheduler.start();
来启动调度器,这样我们的任务就会顺利执行了。
2025-06-26 20:48:52:052
2025-06-26 20:48:53:053
2025-06-26 20:48:54:054
2025-06-26 20:48:55:055
2025-06-26 20:48:56:056
调度器的启动顺序并不重要,你也可以先启动调度器,然后再注册任务。
了解触发器
在Quartz中SimpleScheduleBuilder
还有更多的其他方法:
repeatSecondlyForever()
每秒执行一次,无限执行repeatSecondlyForever(x)
每x秒执行一次,无限执行repeatSecondlyForTotalCount(x)
每秒执行一次,总共执行x次repeatSecondlyForTotalCount(x, y)
每y秒执行一次,总共执行x次还有
repeatMinutelyForever
、repeatHourlyForever
等
除了这些方法以外,Quartz还内置了DailyTimeIntervalScheduleBuilder
、CalendarIntervalScheduleBuilder
和CronScheduleBuilder
来满足不同的开发需求。另外我们还能使用TriggerBuilder
中的startAt
和endAt
来指定一个任务的开始和结束时间,指定之后任务将只在这个时间段内启用。
例如,如果只想要任务在2025-06-26 22:00:00
和2025-06-26 23:00:00
之间每3秒执行一次,则可以使用这个方法指定时间:
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException, ParseException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
// 每 3 秒执行一次
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(3))
// 指定开始时间
.startAt(DATE_FORMAT.parse("2025-06-26 22:00:00"))
// 指定结束时间
.endAt(DATE_FORMAT.parse("2025-06-26 23:00:00"))
.build()
);
scheduler.start();
}
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println(DATE_FORMAT.format(new Date()));
}
}
}
如果希望每个周六、周日的早上8点执行一次任务,则可以使用CronScheduleBuilder
来完成:
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
// 更改任务触发
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 8 ? * 1,7 *"))
.build()
);
scheduler.start();
}
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println(DATE_FORMAT.format(new Date()));
}
}
}
可以看到代码的其他部分都没有改变,唯一的变动仅仅只是把withSchedule(SimpleScheduleBuilder.repeatSecondlyForever())
替换成了withSchedule(CronScheduleBuilder.cronSchedule("0 0 8 * * ?"))
顺便这里也简单介绍一下Quartz中的cron表达式:
Java(Quartz)
* * * * * ? *
- - - - - - -
| | | | | | |
| | | | | | + year [optional]
| | | | | +----- day of week (1 - 7) sun,mon,tue,wed,thu,fri,sat
| | | | +---------- month (1 - 12) OR jan,feb,mar,apr ...
| | | +--------------- day of month (1 - 31)
| | +-------------------- hour (0 - 23)
| +------------------------- min (0 - 59)
+------------------------------ second (0 - 59)
星号(*):表示匹配任意值。例如,* 在分钟字段中表示每分钟都执行。
逗号(,):用于分隔多个值。例如,1,3,5 在小时字段中表示 1 点、3 点和 5 点执行。
斜线(/):用于指定间隔值。例如,*/5 在分钟字段中表示每 5 分钟执行一次。
连字符(-):用于指定范围。例如,10-20 在日期字段中表示从 10 号到 20 号。
问号(?):仅用于日期和星期几字段,表示不指定具体值,通常用于避免冲突。当指定日期时,星期需要设为?,当指定星期时,日期需要设为?。
“L”代表“Last”。当在星期几字段中使用的时候,可以指定给定月份的结构,例如“最后一个星期五”(5L)。在月日字段中,可以指定一个月的最后一天。
星期几字段可以使用“#”,后面必须跟一个介于1和5之间的数字。例如,5#3表示每个月的第三个星期五。
想要了解更多关于Quartz中cron的知识,可以看这篇文章:https://developer.aliyun.com/article/1349827
如果Quartz的内置类都无法完成我们的需求,我们也可以自己实现一个ScheduleBuilder
来根据需求定制自己的触发器。
使用任务数据
一些善于思考的读者也许会这样想:如果Job实例的作用仅仅只是“做某些事情”的话,那为什么不直接使用Runnable
接口呢?Job
接口中这个execute
方法接收的JobExecutionContext
参数有什么用?
这个问题的答案就是我们在这一小节中要讨论的内容:在Scheduler中注册任务时我们可以使用usingJobData来为任务附加参数,在Job中可以通过JobExecutionContext来获取之前设置的数据。
来看示例代码:
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.usingJobData("jobData", "JOB_DATA")
.usingJobData("jobVersion", 1)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
.usingJobData("triggerData", "TRIGGER_DATA")
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
.build()
);
scheduler.start();
}
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) {
JobDataMap map = context.getMergedJobDataMap();
System.out.println("time: " + DATE_FORMAT.format(new Date()));
System.out.println("jobData: " + map.getString("jobData"));
System.out.println("jobVersion: " + map.getInt("jobVersion"));
System.out.println("triggerData: " + map.getString("triggerData"));
}
}
}
这段代码的输出:
time: 2025-06-26 23:26:17
jobData: JOB_DATA
jobVersion: 1
triggerData: TRIGGER_DATA
当我们使用StdSchedulerFactory
创建Scheduler
时,其内置的JobFactory
会自动扫描并注入数据到我们的Job实例中,如果Job有对应的setter,则数据会调用该方法设置到我们的Job实例中。
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.usingJobData("jobData", "JOB_DATA")
.usingJobData("jobVersion", 1)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
.usingJobData("triggerData", "TRIGGER_DATA")
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
.build()
);
scheduler.start();
}
public static class MyJob implements Job {
private String jobData;
private int jobVersion;
private String triggerData;
@Override
public void execute(JobExecutionContext context) {
System.out.println("time: " + DATE_FORMAT.format(new Date()));
System.out.println("jobData: " + jobData);
System.out.println("jobVersion: " + jobVersion);
System.out.println("triggerData: " + triggerData);
}
public void setJobData(String jobData) {
this.jobData = jobData;
System.out.println("set jobData");
}
public void setJobVersion(int jobVersion) {
this.jobVersion = jobVersion;
System.out.println("set jobVersion");
}
public void setTriggerData(String triggerData) {
this.triggerData = triggerData;
System.out.println("set triggerData");
}
}
}
这段代码的输出如下:
set triggerData
set jobData
set jobVersion
time: 2025-06-26 23:30:38
jobData: JOB_DATA
jobVersion: 1
triggerData: TRIGGER_DATA
另外,Quartz为Job准备了两个注解:
@DisallowConcurrentExecution
:禁止该任务并发执行。当为Job类添加了这个注解时,该Job类在同一时刻只会有一个实例被执行。@PersistJobDataAfterExecution
:在任务执行结束后,保存任务对JobDetail中JobDataMap的修改,使得下一次任务执行时可以获取到上一次任务修改的结果。
这两个注解通常是一起使用的,@PersistJobDataAfterExecution
可以让我们的任务持久化保存一些数据,而@DisallowConcurrentExecution
则能够保证我们的任务不会并发执行,避免了数据冲突。
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(3,1))
.build()
);
scheduler.start();
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class MyJob implements Job {
private int persistData = 0;
@Override
public void execute(JobExecutionContext context) {
persistData++;
System.out.println("time: " + DATE_FORMAT.format(new Date()));
System.out.println("persistData: " + persistData);
// 必须手动修改 JobDetail 中的 JobDataMap
// 且必须添加 @PersistJobDataAfterExecution 注解
// 否则对 JobDataMap 的修改不会被保存
context.getJobDetail().getJobDataMap().put("persistData", persistData);
}
public int getPersistData() {
// 注意:虽然 Quartz 会自动调用 setter 为 Job 注入数据
// 但并不会自动调用 getter 保存数据
System.out.println("get persistData");
return persistData;
}
public void setPersistData(int persistData) {
this.persistData = persistData;
System.out.println("set persistData");
}
}
}
这段代码的执行结果:
time: 2025-06-26 23:40:29
persistData: 1
time: 2025-06-26 23:40:30
persistData: 2
time: 2025-06-26 23:40:31
persistData: 3
使用JobStore保存任务数据
即使Quartz能够对JobDataMap
进行操作,使我们的任务能够持久化保存某些数据,一些读者可能仍然会觉得多此一举:因为我们使用Runnable
也同样能够持久化保存数据。
为什么Quartz要单独设计一个Job
类出来?这个JobExecutionContext
是否还有其他的功能呢?这一小节我们来揭晓答案。
JobStore
负责保存Scheduler中的Trigger、Job以及Calendar等。默认情况下Quartz使用的是RAMJobStore
,这意味着所有的数据全部都保存在程序的内存中,当程序关闭时,将会丢失所有的调度信息,这在某些项目中是可以接受的、甚至是必要的。
然而,若要在分布式系统中跨节点保存、同步我们的信息,则可以使用JDBC JobStore,它通过JDBC将任务数据保存在数据库中。
要想使用JDBC JobStore,首先我们要创建对应的数据库表格,MySQL的建表语句可以在这里找到:Github
在数据库中创建好对应的表格后,我们需要通过Properties
来为Quartz的StdSchedulerFactory
设置参数。请查看下面这部分代码:
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
StdSchedulerFactory factory = new StdSchedulerFactory();
Properties properties = new Properties();
// 首先,我们需要设置该实例的名称和 id
properties.setProperty("org.quartz.scheduler.instanceName", "MyClusteredScheduler");
// id 不能与其他实例冲突,否则会导致运行异常。设置为 AUTO 将会自动生成一个不重复的 id
properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");
// 设置 Quartz 运行任务时使用的线程池
// 参数文档:https://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigThreadPool.html
properties.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
properties.setProperty("org.quartz.threadPool.threadCount", "5");
properties.setProperty("org.quartz.threadPool.threadPriority", "5");
// 配置 JobStore
// 参数文档:https://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigJobStoreTX.html
properties.setProperty("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
properties.setProperty("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
properties.setProperty("org.quartz.jobStore.useProperties", "false");
// 指定使用的连接池名称为 myDS
properties.setProperty("org.quartz.jobStore.dataSource", "myDS");
properties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_");
// 关键:开启集群模式
properties.setProperty("org.quartz.jobStore.isClustered", "true");
// 集群模式下,检查任务间隔时间
properties.setProperty("org.quartz.jobStore.clusterCheckinInterval", "1000");
// 为 myDS 连接池进行配置
// 不同连接池的配置可以查看这里:https://github.com/quartz-scheduler/quartz/wiki/How-to-Use-DB-Connection-Pool
// 其他参数的文档:https://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigDataSources.html
properties.setProperty("org.quartz.dataSource.myDS.provider", "hikaricp");
properties.setProperty("org.quartz.dataSource.myDS.driver", "com.mysql.cj.jdbc.Driver");
properties.setProperty("org.quartz.dataSource.myDS.URL", "jdbc:mysql://localhost/test");
properties.setProperty("org.quartz.dataSource.myDS.user", "Test");
properties.setProperty("org.quartz.dataSource.myDS.password", "Test123..");
properties.setProperty("org.quartz.dataSource.myDS.maxConnections", "5");
properties.setProperty("org.quartz.dataSource.myDS.validationQuery", "SELECT 1");
factory.initialize(properties);
Scheduler scheduler = factory.getScheduler();
scheduler.start();
scheduler.scheduleJob(
JobBuilder.newJob(MyJob.class)
.usingJobData("persistData", 0)
.build(),
TriggerBuilder.newTrigger()
.withIdentity("trigger")
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever())
.build()
);
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class MyJob implements Job {
private int persistData;
@Override
public void execute(JobExecutionContext context) {
persistData++;
System.out.println("time: " + DATE_FORMAT.format(new Date()));
System.out.println("persistData: " + persistData);
// 必须手动修改 JobDetail 中的 JobDataMap
// 且必须添加 @PersistJobDataAfterExecution 注解
// 否则对 JobDataMap 的修改不会被保存
context.getJobDetail().getJobDataMap().put("persistData", persistData);
}
public int getPersistData() {
// 注意:虽然 Quartz 会自动调用 setter 为 Job 注入数据
// 但并不会自动调用 getter 保存数据
System.out.println("get persistData");
return persistData;
}
public void setPersistData(int persistData) {
this.persistData = persistData;
System.out.println("set persistData");
}
}
}
第一次启动该程序时,会像上一个版本那样,persistData从1开始计算。
然而,当我们关掉这个程序并再次启动时,输出会像下面这样:
Exception in thread "main" org.quartz.ObjectAlreadyExistsException: Unable to store Trigger with name: 'trigger' and group: 'DEFAULT', because one already exists with this identification.
at org.quartz.impl.jdbcjobstore.JobStoreSupport.storeTrigger(JobStoreSupport.java:1179)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$2.executeVoid(JobStoreSupport.java:1063)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$VoidTransactionCallback.execute(JobStoreSupport.java:3652)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$VoidTransactionCallback.execute(JobStoreSupport.java:3650)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3736)
at org.quartz.impl.jdbcjobstore.JobStoreTX.executeInLock(JobStoreTX.java:95)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.storeJobAndTrigger(JobStoreSupport.java:1058)
at org.quartz.core.QuartzScheduler.scheduleJob(QuartzScheduler.java:841)
at org.quartz.impl.StdScheduler.scheduleJob(StdScheduler.java:250)
at cn.hamster3.quartz.QuartzTest.main(QuartzTest.java:57)
set persistData
time: 2025-06-27 01:06:50
persistData: 19
set persistData
time: 2025-06-27 01:06:51
persistData: 20
set persistData
先有一个报错,这个报错提示我们已经存在一个名称为trigger
的触发器。随后,任务会被正常执行,我们可以看到persistData不再是从1开始输出,而是直接来到了19。
这意味着我们的任务、任务数据(也就是persistData)都已经被成功地保存到了数据库中。如果我们删掉scheduler.scheduleJob
代码,不再为Scheduler注册任务,它也会去到数据库中寻找已经存在的任务,并运行它们。
package cn.hamster3.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
public class QuartzTest {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws SchedulerException {
StdSchedulerFactory factory = new StdSchedulerFactory();
Properties properties = new Properties();
// 设置 properties ...
factory.initialize(properties);
Scheduler scheduler = factory.getScheduler();
scheduler.start();
// 不再手动任务
// scheduler.scheduleJob(
// JobBuilder.newJob(MyJob.class)
// .usingJobData("persistData", 0)
// .build(),
// TriggerBuilder.newTrigger()
// .withIdentity("trigger")
// .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever())
// .build()
// );
}
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public static class MyJob implements Job {
private int persistData;
@Override
public void execute(JobExecutionContext context) {
persistData++;
System.out.println("time: " + DATE_FORMAT.format(new Date()));
System.out.println("persistData: " + persistData);
// 必须手动修改 JobDetail 中的 JobDataMap
// 且必须添加 @PersistJobDataAfterExecution 注解
// 否则对 JobDataMap 的修改不会被保存
context.getJobDetail().getJobDataMap().put("persistData", persistData);
}
public int getPersistData() {
// 注意:虽然 Quartz 会自动调用 setter 为 Job 注入数据
// 但并不会自动调用 getter 保存数据
System.out.println("get persistData");
return persistData;
}
public void setPersistData(int persistData) {
this.persistData = persistData;
System.out.println("set persistData");
}
}
}
控制台中仍然会继续输出:
set persistData
time: 2025-06-27 01:12:01
persistData: 337
set persistData
time: 2025-06-27 01:12:02
persistData: 338
set persistData
time: 2025-06-27 01:12:03
persistData: 339
同时,如果我们在保持这个程序不关闭的情况下,再启动一个相同的程序,你会发现新启动的程序中不会运行这个任务,这是因为Quartz会在执行任务前对其进行加锁,确保任务不会重复执行。
当我们把前一个启动的程序关闭后,由于任务锁被释放,后一个程序在得到了锁之后会继续运行这个任务,你会看到控制台再次出现任务的输出。
踩坑提醒
Job的实现类的访问权限必须是public,且需要保留一个无参的公开构造方法。否则调度器无法正确实例化任务对象,从而导致任务不被执行。
execute方法中仅允许抛出一种类型的异常(包括RuntimeExceptions),即JobExecutionException。因此,你应该将execute方法中的所有内容都放到一个”try-catch”块中。你也应该花点时间看看JobExecutionException的文档,因为你的job可以使用该异常告诉scheduler,你希望如何来处理发生的异常。
如果你想要更深入地学习这个框架,推荐阅读官方文档:https://www.quartz-scheduler.org/documentation/
如果你对英文文档感到恐惧,那么这里有一篇中文文档:https://www.w3cschool.cn/quartz_doc/