Javaにおけるタスクスケジューリング手法の比較
Javaで定期的な処理を実装する方法は複数存在します。それぞれの特徴を整理します。
- Timer: シンプルで導入が容易ですが、単一スレッドでタスクを直列実行するため、1つのタスクが遅延や例外を起こすと後続タスクに影響します。複雑なスケジューリングには不向きです。
- ScheduledExecutor: Java 5で導入されたスレッドプールベースの仕組みです。各タスクはプール内のスレッドで並列実行されるため、相互影響を受けません。ただし、複雑なスケジュールを実装するにはCalendarクラスと組み合わせる必要があり、やや煩雑です。
- Spring Scheduler: Springが提供する簡易スケジューラです。実行時間の指定は可能ですが、タスクキューやスレッドプールの制御力は限定的で、小規模なバッチ処理に適しています。
- JCronTab: Unix系crontab形式の構文でタスクを定義できるライブラリです。ファイル、データベース、XMLでの永続化、メール通知機能、高い拡張性を備えています。
- Quartz: 強力なスケジューリング機能を持ち、Springとの連携が容易です。障害時にもタスク状態を保持する永続化、柔軟なトリガー設定、分散・クラスタリング対応が特徴です。
Quartzの概要
Quartzはオープンソースのタスクスケジューリングフレームワークで、Javaで実装されています。Timerと比較して、ジョブの永続化(状態保持)やジョブ管理機能を備えています。
Quartzの主要構成要素
- Scheduler: タスクスケジューリングの中心的な制御を行うインターフェースです。
- Job: 実際のビジネスロジックを実装するインターフェースで、
executeメソッドに処理を記述します。 - JobDetail: Jobのインスタンスを定義するオブジェクトで、Quartzでは通常JobDetailを指して「Job」と呼びます。
- Trigger: ジョブの実行タイミングを定義するオブジェクトです。
- JobBuilder / TriggerBuilder: JobDetailおよびTriggerインスタンスを生成するためのビルダークラスです。
Spring BootとQuartzの統合手順
1. 依存関係の追加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2. 設定ファイルの記述(application.yml)
spring:
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/my_test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: admin_123
async-init: true
initial-size: 5
min-idle: 5
max-active: 50
max-wait: 6000
pool-prepared-statements: false
max-open-prepared-statements: 20
validation-query: select 1
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
max-evictable-idle-time-millis: 172800000
keep-alive: true
time-between-log-stats-millis: 86400000
filter:
stat:
enabled: true
log-slow-sql: true
slow-sql-millis: 3000
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
quartz:
job-store-type: jdbc
wait-for-jobs-to-complete-on-shutdown: false
overwrite-existing-jobs: true
auto-startup: true
startup-delay: 0s
jdbc:
initialize-schema: always
properties:
org:
quartz:
scheduler:
instanceName: QuartzScheduler
instanceId: AUTO
jobStore:
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
dataSource: quartz
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
3. データベーステーブルの作成
Quartzは11のシステムテーブルを必要とします。設定でinitialize-schema: alwaysを指定すると、アプリ起動時に自動生成されます。生成後はneverに変更してください。または、Quartz公式サイトから提供されるSQLスクリプトを直接実行してテーブルを作成することも可能です。
| テーブル名 | 説明 |
|---|---|
| QRTZ_BLOB_TRIGGERS | Blob型で保存されたトリガー |
| QRTZ_CALENDARS | カレンダー情報(時間範囲の指定に使用) |
| QRTZ_CRON_TRIGGERS | Cron式のトリガー |
| QRTZ_FIRED_TRIGGERS | 実行済みトリガー |
| QRTZ_JOB_DETAILS | JobDetail情報 |
| QRTZ_JOB_LISTENERS | ジョブリスナー |
| QRTZ_LOCKS | 悲観ロック情報 |
| QRTZ_PAUSED_TRIGGER_GRPS | 一時停止中のトリガー |
| QRTZ_SCHEDULER_STATE | スケジューラ状態 |
| QRTZ_SIMPLE_TRIGGERS | シンプルトリガー情報 |
| QRTZ_TRIGGER_LISTENERS | トリガーリスナー |
4. サンプル実装
Serviceインターフェース
public interface QuartzScheduleService {
String registerCronJob(String taskName, String cronExpression, String jobClassPath);
String removeCronJob(String taskName, String taskGroup, String triggerName, String triggerGroup);
String runJobOnce(String taskName, String jobClassPath);
}
Service実装クラス
@Service
@Slf4j
public class QuartzScheduleServiceImpl implements QuartzScheduleService {
@Autowired
private Scheduler scheduler;
private static final String DEFAULT_GROUP = "default_job_group";
private static final String DEFAULT_TRIGGER_GROUP = "default_trigger_group";
private static final String TRIGGER_PREFIX = "Trigger_";
@Override
public String registerCronJob(String taskName, String cronExpression, String jobClassPath) {
try {
JobKey jobKey = JobKey.jobKey(taskName, DEFAULT_GROUP);
if (scheduler.checkExists(jobKey)) {
log.warn("ジョブは既に存在します: {}", jobKey);
return "EXISTS";
}
JobDetail jobDetail = JobBuilder.newJob(resolveJobClass(jobClassPath).getClass())
.withIdentity(jobKey)
.build();
CronScheduleBuilder cronBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(TriggerKey.triggerKey(TRIGGER_PREFIX + taskName, DEFAULT_TRIGGER_GROUP))
.withSchedule(cronBuilder)
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
return "SUCCESS";
} catch (Exception e) {
log.error("ジョブ登録失敗: ", e);
return "FAIL";
}
}
@Override
public String removeCronJob(String taskName, String taskGroup, String triggerName, String triggerGroup) {
try {
JobKey jobKey = JobKey.jobKey(taskName, taskGroup);
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);
if (scheduler.getTrigger(triggerKey) == null) {
log.info("トリガーが見つかりません: triggerName={}, triggerGroup={}", triggerName, triggerGroup);
return "SUCCESS";
}
scheduler.pauseTrigger(triggerKey);
scheduler.unscheduleJob(triggerKey);
scheduler.deleteJob(jobKey);
log.info("ジョブを削除しました: jobName={}, jobGroup={}", taskName, taskGroup);
return "SUCCESS";
} catch (SchedulerException e) {
log.error("ジョブ削除失敗: ", e);
return "FAIL";
}
}
@Override
public String runJobOnce(String taskName, String jobClassPath) {
try {
JobKey jobKey = JobKey.jobKey(taskName, DEFAULT_GROUP);
JobDetail jobDetail = JobBuilder.newJob(resolveJobClass(jobClassPath).getClass())
.withIdentity(jobKey)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(TriggerKey.triggerKey(TRIGGER_PREFIX + taskName, DEFAULT_TRIGGER_GROUP))
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
return "SUCCESS";
} catch (Exception e) {
log.error("即時実行失敗: ", e);
return "FAIL";
}
}
private Job resolveJobClass(String className) throws Exception {
Class<?> clazz = Class.forName(className);
return (Job) clazz.getDeclaredConstructor().newInstance();
}
}
ビジネスジョブ
@Component
@Slf4j
public class SampleTask implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("=== 業務処理の実行開始 ===");
log.info("Job名: {}", context.getJobDetail().getKey().getName());
log.info("Jobグループ: {}", context.getJobDetail().getKey().getGroup());
log.info("トリガー名: {}", context.getTrigger().getKey().getName());
log.info("トリガーグループ: {}", context.getTrigger().getKey().getGroup());
log.info("前回の実行時刻: {}", context.getPreviousFireTime());
log.info("今回の実行時刻: {}", context.getFireTime());
log.info("次回の実行時刻: {}", context.getNextFireTime());
log.info("スケジュール時刻: {}", context.getScheduledFireTime());
}
}
リクエストボディ
@Data
public class TaskRequest {
private String taskName;
private String cronExpression;
private String taskGroup;
private String triggerName;
private String triggerGroup;
}
RESTコントローラ
@RestController
@RequestMapping("/schedule")
public class ScheduleController {
@Autowired
private QuartzScheduleService scheduleService;
@PostMapping("/create")
public String createTask(@RequestBody TaskRequest request) {
return scheduleService.registerCronJob(
request.getTaskName(),
request.getCronExpression(),
"com.example.quartz.SampleTask"
);
}
@PostMapping("/delete")
public String deleteTask(@RequestBody TaskRequest request) {
return scheduleService.removeCronJob(
request.getTaskName(),
request.getTaskGroup(),
request.getTriggerName(),
request.getTriggerGroup()
);
}
@PostMapping("/run-once")
public String runOnce(@RequestBody TaskRequest request) {
return scheduleService.runJobOnce(
request.getTaskName(),
"com.example.quartz.SampleTask"
);
}
}
5. その他の操作用メソッド
// トリガーの一時停止
scheduler.pauseTrigger(TriggerKey.triggerKey("triggerName", "triggerGroup"));
// トリガーの再開
scheduler.resumeTrigger(TriggerKey.triggerKey("triggerName", "triggerGroup"));