今天讲一下怎样用Java实现实时的监控文件行尾的追加内容,类似Linux命令

tail -f

在之前的面试中遇到过一个问题,就是用Java实现tail功能,之前的做法是做一个定时任务每隔1秒去读取一次文件,去判断内容是否有追加,如果有则输出新追加的内容,这个做法虽然能勉强实现功能,但是有点太low,今天采用另外一种实现方式,基于事件通知。

1.WatchService

首先介绍一下WatchService类,WatchService可以监控某一个目录下的文件的变动(新增,修改,删除)并以事件的形式通知文件的变更,这里我们可以实时的获取到文件的修改事件,然后计算出追加的内容,Talk is cheap,Show me the code.

Listener

简单的接口,只有一个fire方法,当事件发生时处理事件。

public interface Listener {
/**
* 发生文件变动事件时的处理逻辑
*
* @param event
*/
void fire(FileChangeEvent event);
}
FileChangeListener
Listener接口的实现类,处理文件变更事件。
public class FileChangeListener implements Listener {
/**
* 保存路径跟文件包装类的映射
*/
private final Map map = new ConcurrentHashMap<>();
public void fire(FileChangeEvent event) {
switch (event.getKind().name()) {
case "ENTRY_MODIFY":
// 文件修改事件
modify(event.getPath());
break;
default:
throw new UnsupportedOperationException(
String.format("The kind [%s] is unsupport.", event.getKind().name()));
}
}
private void modify(Path path) {
// 根据全路径获取包装类对象
FileWrapper wrapper = map.get(path.toString());
if (wrapper == null) {
wrapper = new FileWrapper(path.toFile());
map.put(path.toString(), wrapper);
}
try {
// 读取追加的内容
new ContentReader(wrapper).read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileWrapper

文件包装类,包含文件和当前读取的行号

public class FileWrapper {
/**
* 当前文件读取的行数
*/
private int currentLine;
/**
* 监听的文件
*/
private final File file;
public FileWrapper(File file) {
this(file, 0);
}
public FileWrapper(File file, int currentLine) {
this.file = file;
this.currentLine = currentLine;
}
public int getCurrentLine() {
return currentLine;
}
public void setCurrentLine(int currentLine) {
this.currentLine = currentLine;
}
public File getFile() {
return file;
}
}
FileChangeEvent

文件变更事件

public class FileChangeEvent {
/**
* 文件全路径
*/
private final Path path;
/**
* 事件类型
*/
private final WatchEvent.Kind> kind;
public FileChangeEvent(Path path, Kind> kind) {
this.path = path;
this.kind = kind;
}
public Path getPath() {
return this.path;
}
public WatchEvent.Kind> getKind() {
return this.kind;
}
}
ContentReader

内容读取类

public class ContentReader {
private final FileWrapper wrapper;
public ContentReader(FileWrapper wrapper) {
this.wrapper = wrapper;
}
public void read() throws FileNotFoundException, IOException {
try (LineNumberReader lineReader = new LineNumberReader(new FileReader(wrapper.getFile()))) {
List contents = lineReader.lines().collect(Collectors.toList());
if (contents.size() > wrapper.getCurrentLine()) {
for (int i = wrapper.getCurrentLine(); i < contents.size(); i++) {
// 这里只是简单打印出新加的内容到控制台
System.out.println(contents.get(i));
}
}
// 保存当前读取到的行数
wrapper.setCurrentLine(contents.size());
}
}
}
DirectoryTargetMonitor
目录监视器,监控目录下文件的变化
public class DirectoryTargetMonitor {
private WatchService watchService;
private final FileChangeListener listener;
private final Path path;
private volatile boolean start = false;
public DirectoryTargetMonitor(final FileChangeListener listener, final String targetPath) {
this(listener, targetPath, "");
}
public DirectoryTargetMonitor(final FileChangeListener listener, final String targetPath, final String... morePaths) {
this.listener = listener;
this.path = Paths.get(targetPath, morePaths);
}
public void startMonitor() throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
// 注册变更事件到WatchService
this.path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
this.start = true;
while (start) {
WatchKey watchKey = null;
try {
// 阻塞直到有事件发生
watchKey = watchService.take();
watchKey.pollEvents().forEach(event -> {
WatchEvent.Kind> kind = event.kind();
Path path = (Path) event.context();
Path child = this.path.resolve(path);
listener.fire(new FileChangeEvent(child, kind));
});
} catch (Exception e) {
this.start = false;
} finally {
if (watchKey != null) {
watchKey.reset();
}
}
}
}
public void stopMonitor() throws IOException {
System.out.printf("The directory [%s] monitor will be stop ...\n", path);
Thread.currentThread().interrupt();
this.start = false;
this.watchService.close();
System.out.printf("The directory [%s] monitor will be stop done.\n", path);
}
}

测试类

在D盘新建一个monitor文件夹, 新建一个test.txt文件,然后启动程序,程序启动完成后,我们尝试往test.txt添加内容然后保存,控制台会实时的输出我们追加的内容,PS:追加的内容要以新起一行的形式追加,如果只是在原来的尾行追加,本程序不会输出到控制台,有兴趣的同学可以扩展一下

public static void main(String[] args) throws IOException {
DirectoryTargetMonitor monitor = new DirectoryTargetMonitor(new FileChangeListener(), "D:\\monitor");
monitor.startMonitor();
}