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

Java 實時監控日誌文件變化並輸出新增內容(RandomAccessFile 實現)

     編輯:Java 相關知識

需求:遠程執行 Shell 腳本,將輸出實時寫入日誌文件,前端頁面需實時展示日誌新增內容(類似 `tail -f`)。本文提供基於 `RandomAccessFile` 的完整解決方案,通過記錄文件指針位置,實現增量讀取、編碼處理、定時輪詢,附帶生產者-消費者模擬代碼。

一、核心思路:RandomAccessFile + 文件指針記錄

  • `RandomAccessFile` 支持隨機訪問,`seek(pos)` 定位到上次讀取位置。
  • 維護一個 `lastPosition` 變量,記錄上次讀取到的文件字節偏移量。
  • 定時輪詢:`seek(lastPosition)` → 循環 `readLine()` → 更新 `lastPosition = file.length()`。
  • 編碼處理:`readLine()` 返回 ISO-8859-1 字節串,需轉換為目標編碼(UTF-8/GBK)。

二、完整代碼實現

1. 日誌生產者(模擬 Shell 輸出寫入)

package com.example.logmonitor;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

public class LogProducer {
    private final File logFile;
    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    public LogProducer(File logFile) {
        this.logFile = logFile;
        if (!logFile.exists()) {
            try { logFile.createNewFile(); } catch (IOException e) { throw new RuntimeException(e); }
        }
    }

    public void start() {
        scheduler.scheduleAtFixedRate(() -> {
            try (Writer writer = new FileWriter(logFile, true)) {
                String msg = sdf.format(new Date()) + "\t業務處理成功,訂單號: " + System.currentTimeMillis() + "\n";
                writer.write(msg);
                writer.flush();
                System.out.println("[生產] " + msg.trim());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }, 0, 5, TimeUnit.SECONDS); // 每 5 秒寫一行
    }

    public void stop() { scheduler.shutdown(); }

    public static void main(String[] args) throws Exception {
        File logFile = new File("monitor.log");
        LogProducer producer = new LogProducer(logFile);
        producer.start();
        // 主線程保持運行
        Thread.currentThread().join();
    }
}

2. 日誌消費者(實時讀取新增內容)

package com.example.logmonitor;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.*;

public class LogConsumer {
    private final File logFile;
    private long lastPosition = 0; // 上次讀取位置
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    public LogConsumer(File logFile) {
        this.logFile = logFile;
    }

    public void start() {
        scheduler.scheduleAtFixedRate(() -> {
            try (RandomAccessFile raf = new RandomAccessFile(logFile, "r")) {
                // 文件被截斷/旋轉:長度小於上次位置,重置為 0
                if (raf.length() < lastPosition) {
                    lastPosition = 0;
                }
                raf.seek(lastPosition);
                String line;
                while ((line = raf.readLine()) != null) {
                    // 關鍵:readLine() 返回 ISO-8859-1 編碼的字符串,需轉碼
                    String decoded = new String(line.getBytes("ISO-8859-1"), "UTF-8");
                    System.out.println("[消費] " + decoded);
                }
                lastPosition = raf.getFilePointer(); // 或 raf.length()
            } catch (IOException e) {
                System.err.println("讀取日誌異常: " + e.getMessage());
            }
        }, 0, 1, TimeUnit.SECONDS); // 每 1 秒輪詢一次
    }

    public void stop() { scheduler.shutdown(); }

    public static void main(String[] args) throws Exception {
        File logFile = new File("monitor.log");
        LogConsumer consumer = new LogConsumer(logFile);
        consumer.start();
        Thread.currentThread().join();
    }
}

三、關鍵技術點解析

1. RandomAccessFile 模式選擇

  • "r":只讀模式,消費者足夠,避免意外修改日誌。
  • "rw":讀寫模式,若需在同一進程內寫入並讀取可用。

2. 編碼轉換陷阱

// 錯誤:直接打印 line,中文會亂碼
System.out.println(line); 

// 正確:readLine() 按 ISO-8859-1 解碼字節,需轉回原編碼
String decoded = new String(line.getBytes("ISO-8859-1"), "UTF-8");
// 或指定日誌寫入編碼為 UTF-8,讀取時同樣用 UTF-8 解碼

3. 日誌輪轉/截斷處理

if (raf.length() < lastPosition) {
    // 文件被 logrotate 移動/截斷,長度變小
    lastPosition = 0; // 從頭讀取新文件
}

4. 性能與資源管理

  • 每次輪詢 `new RandomAccessFile()` 開關文件,簡單但有開銷。高頻場景可保持單實例,僅 `seek`/`readLine`,需處理文件句柄泄漏。
  • 輪詢間隔 1 秒平衡實時性與 CPU;極高實時性需求可用 `WatchService` (JDK 7+) 監聽文件修改事件觸發讀取。

四、生產環境增強建議

  1. WebSocket 推送:後端讀取新行後,通過 WebSocket 推送給前端,替代前端輪詢。
  2. 多行日誌合併:堆疊追蹤等多行日誌需正則識別合併後再推送。
  3. 背壓控制:日誌產生過快時,限制前端接收頻率或啟用緩衝區。
  4. 安全性:限制可讀取的日誌目錄,防止路徑遍歷讀取敏感文件。

五、總結

`RandomAccessFile` + `seek(lastPosition)` + `readLine()` 循環 + 編碼轉換,構成了 Java 實現 `tail -f` 功能的核心模式。關鍵細節:記錄字節偏移量而非行號、處理文件截斷、正確轉換 `readLine()` 的 ISO-8859-1 編碼。該方案輕量、無第三方依賴,適合中小型項目快速集成實時日誌查看功能。

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