Web 應用中常需定時執行任務:數據同步、報表生成、緩存預熱、清理過期數據。本文展示基於 java.util.Timer + ServletContextListener 的輕量級方案,涵蓋啟動延遲、週期執行、優雅關閉、異常隔離等生產級細節。
┌─────────────────────────────────────┐
│ ServletContextListener │ ← Web 容器生命週期掛鉤
│ contextInitialized() → timer.start()│
│ contextDestroyed() → timer.cancel()│
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Timer │ ← 單一後台執行緒 (daemon)
│ schedule(TimerTask, delay, period) │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ TimerTask (Runnable) │ ← 實際業務邏輯
│ run() { doBusinessLogic(); } │
└─────────────────────────────────────┘
public class DataSyncTask extends TimerTask {
private static final Logger log = LoggerFactory.getLogger(DataSyncTask.class);
private final DataSyncService syncService; // 注入業務服務
public DataSyncTask(DataSyncService syncService) {
this.syncService = syncService;
}
@Override
public void run() {
try {
log.info("定時任務開始執行: {}", new Date());
syncService.syncAll(); // 核心業務
log.info("定時任務執行完成");
} catch (Exception e) {
log.error("定時任務執行異常", e); // 吞掉異常,防止 Timer 執行緒終止
}
}
}
public class AppTimerManager {
private Timer timer;
private final DataSyncTask syncTask;
public AppTimerManager(DataSyncTask syncTask) {
this.syncTask = syncTask;
}
public void start() {
if (timer != null) return; // 防重複啟動
timer = new Timer(true); // true = daemon 執行緒,不阻塞 JVM 退出
// 計算首次執行延遲:每天 23:00 執行
Date firstRun = calculateNextRunTime(23, 0, 0);
long initialDelay = firstRun.getTime() - System.currentTimeMillis();
if (initialDelay < 0) initialDelay += 24 * 60 * 60 * 1000; // 已過則順延明天
long period = 24 * 60 * 60 * 1000L; // 24 小時週期
timer.schedule(syncTask, initialDelay, period);
log.info("定時器已啟動,首次執行時間: {}, 週期: 24h", firstRun);
}
public void stop() {
if (timer != null) {
timer.cancel();
timer = null;
log.info("定時器已停止");
}
}
private Date calculateNextRunTime(int hour, int minute, int second) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, second);
cal.set(Calendar.MILLISECOND, 0);
if (cal.getTimeInMillis() <= System.currentTimeMillis()) {
cal.add(Calendar.DAY_OF_MONTH, 1);
}
return cal.getTime();
}
}
@WebListener
public class TimerContextListener implements ServletContextListener {
private AppTimerManager timerManager;
@Override
public void contextInitialized(ServletContextEvent sce) {
// 從 Spring 容器獲取 Bean (需 WebApplicationContextUtils)
WebApplicationContext ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(sce.getServletContext());
DataSyncTask task = ctx.getBean(DataSyncTask.class);
timerManager = new AppTimerManager(task);
timerManager.start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (timerManager != null) {
timerManager.stop();
}
}
}
<listener>
<listener-class>com.example.TimerContextListener</listener-class>
</listener>
| 要點 | 做法 | 理由 |
|---|---|---|
| Daemon 執行緒 | new Timer(true) | 不阻塞 JVM 關閉,容器停止時無需強制殺進程 |
| 異常隔離 | run() 內 try-catch 並記錄日誌 | 防止單次任務異常導致 Timer 執行緒死亡,後續任務不再執行 |
| 防重複啟動 | start() 先檢查 timer != null | 熱部署、多次 contextInitialized 時避免多個 Timer 並存 |
| 優雅關閉 | contextDestroyed() 調用 timer.cancel() | 取消待執行任務,等待當前任務完成 (cancel() 不中斷正在運行的 task) |
| 首次延遲計算 | 動態計算下一個目標時間點 | 避免部署時間導致首次立即執行或錯過排程 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Timer + Listener | 零依賴、輕量、標準 API | 單執行緒、無持久化、無分散式支援、無監控 UI | 單體應用、簡單定時需求 |
| Spring @Scheduled | 聲明式、支援 SpEL、整合 Spring 生命週期、線程池 | 需 Spring 容器、單機模式 | Spring/Spring Boot 專案首選 |
| Quartz | 持久化、集群、Cron 表達式、監控 API | 配置複雜、需資料庫、較重 | 高可靠、分散式、動態增減任務 |
| XXL-JOB | 分散式、Web 控制台、分片廣播、故障轉移 | 需獨立調度中心、運維成本 | 微服務架構、跨語言、可視化運維 |
Timer + ServletContextListener 是零依賴實現 Web 定時任務的經典模式。核心要點:daemon 執行緒、異常吞噬、動態首次延遲、優雅關閉。隨業務增長,建議遷移至 Spring @Scheduled 或 Quartz/XXL-JOB,獲得持久化、集群、監控等企業級能力。