当文件监控成为"隐形守护者"

“又出问题了?”

这是我作为Java后端工程师,凌晨三点被线上报警惊醒时的内心独白。打开日志,看到的是一堆零散的文件操作记录:

2023-08-07 03:15:22.345 INFO [file-sync] Processing file: config.properties
2023-08-07 03:15:23.789 INFO [file-sync] Processing file: log.txt
2023-08-07 03:15:25.123 INFO [file-sync] Processing file: data.csv

问题来了:这些文件变化到底是谁触发的? 为什么系统在凌晨三点突然处理这些文件?我手忙脚乱地在代码中加了文件轮询,结果发现:

  • 每次轮询都遍历整个目录,性能极差
  • 文件数量多时,轮询时间长到可怕
  • 无法实时响应文件变化

直到我遇到了Java WatchService。

这不是一个简单的库,而是一个让文件监控变得"潜伏到骨子里"的解决方案。它不仅仅是一个工具,更像是一个"文件系统中的卧底",默默守护着文件变化,而不需要你手动轮询。

为什么WatchService是文件监控的"最佳卧底"

一、WatchService的"卧底"优势:不是"轮询",而是"监听"

WatchService的工作原理是通过操作系统原生文件系统来监控文件变化,而不是像传统方法那样轮询遍历文件。

传统轮询的"地狱模式":

// 传统文件监控方式 - 轮询遍历
public class FileMonitor {
    private final Path directory;
    private final Set<String> lastFiles = new HashSet<>();
    
    public FileMonitor(String directoryPath) {
        this.directory = Paths.get(directoryPath);
    }
    
    public void startMonitoring() {
        new Thread(() -> {
            while (true) {
                try {
                    // 1. 每次轮询都遍历整个目录
                    Set<String> currentFiles = getFilesInDirectory();
                    
                    // 2. 比较文件变化
                    Set<String> newFiles = Sets.difference(currentFiles, lastFiles);
                    Set<String> deletedFiles = Sets.difference(lastFiles, currentFiles);
                    
                    // 3. 处理文件变化
                    for (String file : newFiles) {
                        System.out.println("New file: " + file);
                        // 处理新文件
                    }
                    
                    for (String file : deletedFiles) {
                        System.out.println("Deleted file: " + file);
                        // 处理删除文件
                    }
                    
                    // 4. 更新上一次的文件状态
                    lastFiles = currentFiles;
                    
                    // 5. 等待一段时间再轮询
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();
    }
    
    private Set<String> getFilesInDirectory() {
        try (Stream<Path> stream = Files.list(directory)) {
            return stream.map(Path::getFileName)
                         .map(FileName::toString)
                         .collect(Collectors.toSet());
        } catch (IOException e) {
            throw new RuntimeException("Error listing directory", e);
        }
    }
}

注释: 这个传统轮询实现的问题显而易见:

  • 每次轮询都遍历整个目录,性能极差
  • 文件数量多时,遍历时间长到可怕
  • 无法实时响应文件变化
  • 需要手动管理文件状态
  • 无法处理并发文件操作

WatchService的"潜伏"实现:

// WatchService实现 - 优雅的文件监控
public class FileWatchDog implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(FileWatchDog.class);
    private final Path directory;
    private final WatchService watchService;
    
    public FileWatchDog(String directoryPath) throws IOException {
        this.directory = Paths.get(directoryPath);
        this.watchService = FileSystems.getDefault().newWatchService();
        
        // 注册目录监控
        directory.register(watchService,
                StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY,
                StandardWatchEventKinds.ENTRY_DELETE);
        
        logger.info("File monitor started for directory: {}", directory);
    }
    
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                // 1. 获取下一个事件(阻塞等待)
                WatchKey key = watchService.take();
                
                // 2. 处理所有事件
                for (WatchEvent<?> event : key.pollEvents()) {
                    // 3. 获取事件类型和文件名
                    WatchEvent.Kind<?> kind = event.kind();
                    Path fileName = (Path) event.context();
                    
                    // 4. 处理不同类型的事件
                    if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                        handleFileCreate(fileName);
                    } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                        handleFileModify(fileName);
                    } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                        handleFileDelete(fileName);
                    }
                }
                
                // 5. 重置key,准备下一次事件
                boolean valid = key.reset();
                if (!valid) {
                    logger.warn("WatchKey is invalid, exiting monitoring");
                    break;
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.info("File monitor interrupted");
        } catch (Exception e) {
            logger.error("Error in file monitoring", e);
        }
    }
    
    private void handleFileCreate(Path fileName) {
        logger.info("File created: {}", fileName);
        
        // 1. 获取完整路径
        Path filePath = directory.resolve(fileName);
        
        // 2. 检查文件类型(可选)
        if (Files.isRegularFile(filePath)) {
            // 3. 处理新文件
            processNewFile(filePath);
        }
    }
    
    private void handleFileModify(Path fileName) {
        logger.info("File modified: {}", fileName);
        
        // 1. 获取完整路径
        Path filePath = directory.resolve(fileName);
        
        // 2. 检查文件类型(可选)
        if (Files.isRegularFile(filePath)) {
            // 3. 处理修改后的文件
            processModifiedFile(filePath);
        }
    }
    
    private void handleFileDelete(Path fileName) {
        logger.info("File deleted: {}", fileName);
        
        // 1. 获取完整路径
        Path filePath = directory.resolve(fileName);
        
        // 2. 处理删除文件
        processDeletedFile(filePath);
    }
    
    private void processNewFile(Path filePath) {
        // 1. 读取文件内容(示例)
        try {
            String content = new String(Files.readAllBytes(filePath));
            logger.debug("New file content: {}", content);
            
            // 2. 处理文件内容(根据业务需求)
            // 例如:解析配置文件、处理数据等
            if (filePath.getFileName().toString().endsWith(".properties")) {
                processConfigFile(filePath);
            }
        } catch (IOException e) {
            logger.error("Error processing new file: {}", filePath, e);
        }
    }
    
    private void processModifiedFile(Path filePath) {
        // 1. 读取文件内容(示例)
        try {
            String content = new String(Files.readAllBytes(filePath));
            logger.debug("Modified file content: {}", content);
            
            // 2. 处理修改后的文件(根据业务需求)
            // 例如:重新加载配置、更新缓存等
            if (filePath.getFileName().toString().endsWith(".properties")) {
                reloadConfigFile(filePath);
            }
        } catch (IOException e) {
            logger.error("Error processing modified file: {}", filePath, e);
        }
    }
    
    private void processDeletedFile(Path filePath) {
        // 1. 处理删除文件(根据业务需求)
        // 例如:清理缓存、删除相关数据等
        logger.info("Deleting file: {}", filePath);
        
        // 2. 根据文件类型进行处理
        if (filePath.getFileName().toString().endsWith(".properties")) {
            clearConfigCache();
        }
    }
    
    private void processConfigFile(Path filePath) {
        // 1. 解析配置文件
        // 2. 更新应用配置
        logger.info("Processing config file: {}", filePath);
        
        // 3. 实际配置处理逻辑
        // 例如:使用Properties类加载配置
        try (InputStream is = Files.newInputStream(filePath)) {
            Properties props = new Properties();
            props.load(is);
            
            // 4. 更新应用配置
            ApplicationConfig.updateConfig(props);
        } catch (IOException e) {
            logger.error("Error processing config file: {}", filePath, e);
        }
    }
    
    private void reloadConfigFile(Path filePath) {
        // 1. 重新加载配置
        logger.info("Reloading config file: {}", filePath);
        
        // 2. 实际配置重载逻辑
        try (InputStream is = Files.newInputStream(filePath)) {
            Properties props = new Properties();
            props.load(is);
            
            // 3. 更新应用配置
            ApplicationConfig.reloadConfig(props);
        } catch (IOException e) {
            logger.error("Error reloading config file: {}", filePath, e);
        }
    }
    
    private void clearConfigCache() {
        // 1. 清理配置缓存
        logger.info("Clearing config cache");
        ApplicationConfig.clearCache();
    }
}

注释: 这个WatchService实现展示了为什么它比传统轮询"优雅":

  • 实时性:不需要等待轮询间隔,事件发生即被处理
  • 性能:不遍历文件,只处理变化的文件
  • 简洁性:代码清晰,没有轮询和状态比较
  • 可扩展性:很容易添加新的文件处理逻辑

二、WatchService的"潜伏"深度:不只是监控,更是智能处理

WatchService的真正强大之处在于它能与其他Java技术无缝集成,实现"智能"文件监控。

1. 多目录监控:一个卧底,多个目标
public class MultiDirectoryWatchDog implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(MultiDirectoryWatchDog.class);
    private final WatchService watchService;
    private final Map<Path, Set<WatchEvent.Kind<?>>> monitoredDirectories = new HashMap<>();
    
    public MultiDirectoryWatchDog() throws IOException {
        this.watchService = FileSystems.getDefault().newWatchService();
    }
    
    public void addDirectoryMonitor(String directoryPath, 
                                   WatchEvent.Kind<?>... events) throws IOException {
        Path directory = Paths.get(directoryPath);
        monitoredDirectories.put(directory, new HashSet<>(Arrays.asList(events)));
        
        // 注册目录监控
        directory.register(watchService, events);
        
        logger.info("Added directory monitor for: {} with events: {}", 
                    directory, Arrays.toString(events));
    }
    
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                // 1. 获取下一个事件
                WatchKey key = watchService.take();
                
                // 2. 获取事件的目录
                Path directory = (Path) key.watchable();
                
                // 3. 获取事件类型
                WatchEvent.Kind<?> kind = key.pollEvents().get(0).kind();
                
                // 4. 处理事件
                handleEvent(directory, kind, key);
                
                // 5. 重置key
                boolean valid = key.reset();
                if (!valid) {
                    logger.warn("WatchKey for directory {} is invalid", directory);
                    monitoredDirectories.remove(directory);
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.info("Directory monitor interrupted");
        } catch (Exception e) {
            logger.error("Error in directory monitoring", e);
        }
    }
    
    private void handleEvent(Path directory, WatchEvent.Kind<?> kind, WatchKey key) {
        for (WatchEvent<?> event : key.pollEvents()) {
            Path fileName = (Path) event.context();
            
            logger.debug("Event in directory {}: {} - {}", 
                         directory, event.kind(), fileName);
            
            // 1. 根据事件类型处理
            if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                handleFileCreate(directory.resolve(fileName));
            } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                handleFileModify(directory.resolve(fileName));
            } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                handleFileDelete(directory.resolve(fileName));
            }
        }
    }
    
    private void handleFileCreate(Path filePath) {
        // 1. 检查文件类型
        if (Files.isRegularFile(filePath)) {
            logger.info("New file detected: {}", filePath);
            
            // 2. 处理文件
            if (filePath.getFileName().toString().endsWith(".log")) {
                processLogfile(filePath);
            } else if (filePath.getFileName().toString().endsWith(".csv")) {
                processCsvFile(filePath);
            }
        }
    }
    
    private void handleFileModify(Path filePath) {
        // 1. 检查文件类型
        if (Files.isRegularFile(filePath)) {
            logger.info("Modified file: {}", filePath);
            
            // 2. 处理文件
            if (filePath.getFileName().toString().endsWith(".log")) {
                processLogfile(filePath);
            } else if (filePath.getFileName().toString().endsWith(".csv")) {
                processCsvFile(filePath);
            }
        }
    }
    
    private void handleFileDelete(Path filePath) {
        // 1. 检查文件类型
        if (Files.isRegularFile(filePath)) {
            logger.info("Deleted file: {}", filePath);
            
            // 2. 处理删除
            if (filePath.getFileName().toString().endsWith(".log")) {
                cleanupLogfile(filePath);
            } else if (filePath.getFileName().toString().endsWith(".csv")) {
                cleanupCsvFile(filePath);
            }
        }
    }
    
    private void processLogfile(Path filePath) {
        // 1. 读取日志文件
        try (BufferedReader reader = Files.newBufferedReader(filePath)) {
            String line;
            while ((line = reader.readLine()) != null) {
                // 2. 处理日志行
                logger.debug("Processing log line: {}", line);
                // 3. 实际日志处理逻辑
                LogProcessor.processLine(line);
            }
        } catch (IOException e) {
            logger.error("Error processing log file: {}", filePath, e);
        }
    }
    
    private void processCsvFile(Path filePath) {
        // 1. 读取CSV文件
        try (BufferedReader reader = Files.newBufferedReader(filePath)) {
            String header = reader.readLine();
            logger.info("CSV header: {}", header);
            
            // 2. 处理CSV数据
            String line;
            while ((line = reader.readLine()) != null) {
                logger.debug("CSV data: {}", line);
                // 3. 实际CSV处理逻辑
                CsvProcessor.processLine(line);
            }
        } catch (IOException e) {
            logger.error("Error processing CSV file: {}", filePath, e);
        }
    }
    
    private void cleanupLogfile(Path filePath) {
        // 1. 清理日志文件相关资源
        logger.info("Cleaning up log file resources: {}", filePath);
        LogProcessor.cleanup(filePath);
    }
    
    private void cleanupCsvFile(Path filePath) {
        // 1. 清理CSV文件相关资源
        logger.info("Cleaning up CSV file resources: {}", filePath);
        CsvProcessor.cleanup(filePath);
    }
}

注释: 这个多目录监控实现展示了WatchService的"潜伏"能力:

  • 多目标监控:可以同时监控多个目录
  • 事件类型定制:每个目录可以监控不同的事件类型
  • 智能处理:根据文件类型处理不同逻辑
  • 优雅的错误处理:自动处理无效的WatchKey
2. WatchService的"潜伏"技巧:处理文件事件的"潜伏"策略

WatchService在处理文件事件时,有一些"潜伏"技巧可以避免常见问题。

问题:文件修改事件可能被多次触发

当文件被修改时,操作系统可能会触发多次修改事件。这会导致我们的处理逻辑被多次调用。

解决方案:事件合并

public class EventMerger {
    private final Map<Path, Long> lastModifiedTime = new HashMap<>();
    private final long debounceTime = 500; // 毫秒
    
    public boolean isEventDebounced(Path filePath) {
        // 1. 获取文件的最后修改时间
        try {
            BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
            long lastModified = attrs.lastModifiedTime().toMillis();
            
            // 2. 检查是否在去抖动时间内
            if (lastModifiedTime.containsKey(filePath)) {
                long lastTime = lastModifiedTime.get(filePath);
                if (lastModified - lastTime < debounceTime) {
                    return true; // 事件被去抖动
                }
            }
            
            // 3. 更新最后修改时间
            lastModifiedTime.put(filePath, lastModified);
            return false; // 事件未被去抖动
        } catch (IOException e) {
            logger.error("Error checking file modification time: {}", filePath, e);
            return false;
        }
    }
}

注释: 这个EventMerger类使用了"去抖动"技术,避免在短时间内多次处理同一个文件的修改事件。它通过记录文件的最后修改时间,判断是否在去抖动时间内。

如何在WatchService中使用:

// 在FileWatchDog中添加事件合并
private final EventMerger eventMerger = new EventMerger();

@Override
public void run() {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            WatchKey key = watchService.take();
            
            for (WatchEvent<?> event : key.pollEvents()) {
                Path fileName = (Path) event.context();
                Path filePath = directory.resolve(fileName);
                
                // 1. 检查是否是去抖动的事件
                if (eventMerger.isEventDebounced(filePath)) {
                    logger.debug("Debounced event for file: {}", filePath);
                    continue;
                }
                
                // 2. 处理事件
                if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    handleFileModify(filePath);
                }
                // 其他事件类型处理...
            }
            
            boolean valid = key.reset();
            if (!valid) {
                logger.warn("WatchKey is invalid, exiting monitoring");
                break;
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        logger.info("File monitor interrupted");
    }
}

注释: 这段代码展示了如何将事件合并技术集成到WatchService中,避免重复处理同一个文件的修改事件。

3. WatchService的"潜伏"深度:处理文件事件的"智能"策略

WatchService不仅可以监控文件变化,还可以智能地处理这些变化。

问题:文件可能在被写入过程中触发修改事件

当文件正在被写入时,操作系统可能会触发修改事件。这会导致我们处理一个不完整的文件。

解决方案:等待文件写入完成

public class FileWriteCompletion {
    private final Map<Path, Long> lastModifiedTime = new HashMap<>();
    private final long writeTimeout = 2000; // 毫秒
    
    public boolean isFileComplete(Path filePath) {
        try {
            BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
            long lastModified = attrs.lastModifiedTime().toMillis();
            
            // 1. 检查文件是否在写入过程中
            if (lastModifiedTime.containsKey(filePath)) {
                long lastTime = lastModifiedTime.get(filePath);
                if (lastModified - lastTime < writeTimeout) {
                    return false; // 文件仍在写入中
                }
            }
            
            // 2. 更新最后修改时间
            lastModifiedTime.put(filePath, lastModified);
            return true; // 文件已写入完成
        } catch (IOException e) {
            logger.error("Error checking file completion: {}", filePath, e);
            return false;
        }
    }
}

注释: 这个FileWriteCompletion类检查文件是否已完全写入。它记录文件的最后修改时间,并在一定时间内不更新时认为文件已写入完成。

如何在WatchService中使用:

// 在FileWatchDog中添加文件写入完成检查
private final FileWriteCompletion fileWriteCompletion = new FileWriteCompletion();

@Override
public void run() {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            WatchKey key = watchService.take();
            
            for (WatchEvent<?> event : key.pollEvents()) {
                Path fileName = (Path) event.context();
                Path filePath = directory.resolve(fileName);
                
                // 1. 检查文件是否写入完成
                if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY && 
                    !fileWriteCompletion.isFileComplete(filePath)) {
                    logger.debug("File is still being written: {}", filePath);
                    continue;
                }
                
                // 2. 处理事件
                if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                    handleFileCreate(filePath);
                } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    handleFileModify(filePath);
                } else if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
                    handleFileDelete(filePath);
                }
            }
            
            boolean valid = key.reset();
            if (!valid) {
                logger.warn("WatchKey is invalid, exiting monitoring");
                break;
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        logger.info("File monitor interrupted");
    }
}

注释: 这段代码展示了如何在WatchService中使用文件写入完成检查,避免处理不完整的文件。

三、WatchService的"潜伏"实战:真实应用场景

1. 实时配置文件热加载
public class ConfigHotReload {
    private final FileWatchDog watchDog;
    private final Path configDirectory;
    private final Properties currentConfig = new Properties();
    
    public ConfigHotReload(String configDirectoryPath) throws IOException {
        this.configDirectory = Paths.get(configDirectoryPath);
        this.watchDog = new FileWatchDog(configDirectoryPath);
        
        // 初始化配置
        loadInitialConfig();
        
        // 启动监控
        new Thread(watchDog, "Config-File-Monitor").start();
    }
    
    private void loadInitialConfig() {
        try {
            // 1. 查找配置文件
            List<Path> configFiles = Files.list(configDirectory)
                                        .filter(path -> path.getFileName().toString().endsWith(".properties"))
                                        .collect(Collectors.toList());
            
            // 2. 加载配置
            for (Path configFile : configFiles) {
                try (InputStream is = Files.newInputStream(configFile)) {
                    currentConfig.load(is);
                    logger.info("Loaded config from: {}", configFile);
                }
            }
            
            // 3. 应用配置
            ApplicationConfig.applyConfig(currentConfig);
        } catch (IOException e) {
            logger.error("Error loading initial config", e);
        }
    }
    
    public void handleFileCreate(Path filePath) {
        handleConfigFile(filePath);
    }
    
    public void handleFileModify(Path filePath) {
        handleConfigFile(filePath);
    }
    
    private void handleConfigFile(Path filePath) {
        if (filePath.getFileName().toString().endsWith(".properties")) {
            try {
                // 1. 读取配置文件
                Properties newConfig = new Properties();
                try (InputStream is = Files.newInputStream(filePath)) {
                    newConfig.load(is);
                }
                
                // 2. 合并配置
                Properties mergedConfig = new Properties();
                mergedConfig.putAll(currentConfig);
                mergedConfig.putAll(newConfig);
                
                // 3. 更新配置
                currentConfig.clear();
                currentConfig.putAll(mergedConfig);
                
                // 4. 应用配置
                ApplicationConfig.applyConfig(currentConfig);
                
                logger.info("Config updated from file: {}", filePath);
            } catch (IOException e) {
                logger.error("Error processing config file: {}", filePath, e);
            }
        }
    }
    
    public void handleFileDelete(Path filePath) {
        if (filePath.getFileName().toString().endsWith(".properties")) {
            try {
                // 1. 从配置中移除已删除的文件
                currentConfig.remove(filePath.getFileName().toString());
                
                // 2. 重新应用配置
                ApplicationConfig.applyConfig(currentConfig);
                
                logger.info("Config updated after file deletion: {}", filePath);
            } catch (Exception e) {
                logger.error("Error processing config file deletion: {}", filePath, e);
            }
        }
    }
}

注释: 这个ConfigHotReload类展示了WatchService在实时配置热加载中的应用:

  • 自动加载:当配置文件被创建或修改时,自动加载并应用新配置
  • 配置合并:新配置与现有配置合并,避免覆盖
  • 实时更新:无需重启应用,配置立即生效
2. 文件系统监控与日志分析
public class LogFileMonitor {
    private final FileWatchDog watchDog;
    private final Path logDirectory;
    
    public LogFileMonitor(String logDirectoryPath) throws IOException {
        this.logDirectory = Paths.get(logDirectoryPath);
        this.watchDog = new FileWatchDog(logDirectoryPath);
        
        // 启动监控
        new Thread(watchDog, "Log-File-Monitor").start();
    }
    
    public void handleFileCreate(Path filePath) {
        if (filePath.getFileName().toString().endsWith(".log")) {
            processNewLogFile(filePath);
        }
    }
    
    public void handleFileModify(Path filePath) {
        if (filePath.getFileName().toString().endsWith(".log")) {
            processModifiedLogFile(filePath);
        }
    }
    
    private void processNewLogFile(Path filePath) {
        try {
            // 1. 读取文件内容
            List<String> lines = Files.readAllLines(filePath);
            
            // 2. 处理日志行
            for (String line : lines) {
                processLogLine(line);
            }
            
            logger.info("Processed new log file: {}", filePath);
        } catch (IOException e) {
            logger.error("Error processing new log file: {}", filePath, e);
        }
    }
    
    private void processModifiedLogFile(Path filePath) {
        try {
            // 1. 读取文件内容
            List<String> lines = Files.readAllLines(filePath);
            
            // 2. 处理新增的日志行
            // 例如:从文件末尾开始读取
            long lastProcessedPosition = getLastProcessedPosition(filePath);
            long fileLength = Files.size(filePath);
            
            if (fileLength > lastProcessedPosition) {
                try (BufferedReader reader = Files.newBufferedReader(filePath)) {
                    // 跳过已处理的部分
                    reader.skip(lastProcessedPosition);
                    
                    String line;
                    while ((line = reader.readLine()) != null) {
                        processLogLine(line);
                    }
                    
                    // 3. 更新最后处理位置
                    setLastProcessedPosition(filePath, fileLength);
                }
            }
            
            logger.info("Processed modified log file: {}", filePath);
        } catch (IOException e) {
            logger.error("Error processing modified log file: {}", filePath, e);
        }
    }
    
    private void processLogLine(String line) {
        // 1. 解析日志行
        // 2. 处理日志
        logger.debug("Processing log line: {}", line);
        
        // 3. 实际日志处理逻辑
        // 例如:将日志发送到分析系统
        LogAnalyzer.analyze(line);
    }
    
    // 用于跟踪已处理的日志位置
    private Map<Path, Long> lastProcessedPositions = new HashMap<>();
    
    private long getLastProcessedPosition(Path filePath) {
        return lastProcessedPositions.getOrDefault(filePath, 0L);
    }
    
    private void setLastProcessedPosition(Path filePath, long position) {
        lastProcessedPositions.put(filePath, position);
    }
}

注释: 这个LogFileMonitor类展示了WatchService在日志分析中的应用:

  • 增量处理:只处理新添加的日志行,避免重复处理
  • 高效处理:使用文件位置跟踪,提高处理效率
  • 实时分析:日志实时分析,无需等待文件关闭

WatchService的"潜伏"哲学

Spring Cloud Sleuth之所以"优雅",不是因为它让代码变少了,而是因为它让追踪逻辑从代码中"消失"了。同样,WatchService之所以强大,不是因为它能监控文件,而是因为它让文件监控从"轮询"变为"潜伏"

为什么WatchService是文件监控的"最佳卧底"?

  1. 实时性:不需要等待轮询间隔,事件发生即被处理
  2. 性能:不遍历文件,只处理变化的文件
  3. 简洁性:代码清晰,没有轮询和状态比较
  4. 智能性:可以集成事件合并、文件写入完成检查等高级技术
  5. 灵活性:可以监控多个目录,处理不同类型的事件
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐