首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Web 定時任務實戰:Timer + ServletContextListener 優雅啟停

     編輯:Java 相關知識

Web 應用中常需定時執行任務:數據同步、報表生成、緩存預熱、清理過期數據。本文展示基於 java.util.Timer + ServletContextListener 的輕量級方案,涵蓋啟動延遲、週期執行、優雅關閉、異常隔離等生產級細節。

核心架構

┌─────────────────────────────────────┐
│      ServletContextListener         │  ← Web 容器生命週期掛鉤
│  contextInitialized() → timer.start()│
│  contextDestroyed()   → timer.cancel()│
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│              Timer                  │  ← 單一後台執行緒 (daemon)
│  schedule(TimerTask, delay, period) │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│          TimerTask (Runnable)       │  ← 實際業務邏輯
│  run() { doBusinessLogic(); }       │
└─────────────────────────────────────┘

完整代碼實現

1. 任務執行類

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 執行緒終止
        }
    }
}

2. 定時器管理器

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();
    }
}

3. ServletContextListener 掛鉤

@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();
        }
    }
}

4. web.xml 配置 (非 Spring Boot / 無 @WebListener 時)

<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,獲得持久化、集群、監控等企業級能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved