这是一个 JavaFX 服务,它将使用 WatchService 来监视另一个线程上的事件。
演示该服务的示例应用程序会侦听 WatchService 检测到的更改并将其记录在列表中。但是您可以过滤更改事件,并在收到它们时做任何您想做的事情(例如按照您的意愿打开一个阶段)。如果你愿意,你甚至可以translate the detected events to JavaFX events,尽管它的实现超出了这个答案的范围。
目录WatchService的ScheduledService实现
WatchService 的实现基于watching a file directory 上的Oracle 教程和ScheduledService 上的JavaFX。
import javafx.beans.property.*;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;
/**
* Watch a directory (or tree) for changes to files.
*/
public class WatchDirService extends ScheduledService<List<WatchEvent<Path>>> {
private final WatchService watcher;
private final Map<WatchKey,Path> keys;
private boolean trace;
private ReadOnlyStringWrapper dir = new ReadOnlyStringWrapper(this, "dir");
public final String getDir() { return dir.get(); }
public final ReadOnlyStringProperty dirProperty() { return dir.getReadOnlyProperty(); }
private ReadOnlyBooleanWrapper recursive = new ReadOnlyBooleanWrapper(this, "recursive");
public boolean isRecursive() { return recursive.get(); }
public ReadOnlyBooleanProperty recursiveProperty() { return recursive; }
@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
/**
* Creates a WatchService and registers the given directory
*/
public WatchDirService(Path dir, boolean recursive) throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<>();
this.dir.set(dir.toString());
this.recursive.set(recursive);
if (recursive) {
System.out.format("Scanning %s ...\n", dir);
registerAll(dir);
System.out.println("Done.");
} else {
register(dir);
}
// enable trace after initial registration
this.trace = true;
}
/**
* Register the given directory with the WatchService
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
if (trace) {
Path prev = keys.get(key);
if (prev == null) {
System.out.format("register: %s\n", dir);
} else {
if (!dir.equals(prev)) {
System.out.format("update: %s -> %s\n", prev, dir);
}
}
}
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException
{
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
@Override
protected Task<List<WatchEvent<Path>>> createTask() {
return new WatchTask();
}
class WatchTask extends Task<List<WatchEvent<Path>>> {
@Override
protected List<WatchEvent<Path>> call() {
// wait for key to be signalled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
if (isCancelled()) {
updateMessage("Cancelled");
}
return Collections.emptyList();
}
Path dir = keys.get(key);
if (dir == null) {
System.err.println("WatchKey not recognized");
return Collections.emptyList();
}
List<WatchEvent<Path>> interestingEvents = new ArrayList<>();
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> pathWatchEvent = cast(event);
Path name = pathWatchEvent.context();
Path child = dir.resolve(name);
interestingEvents.add(pathWatchEvent);
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (recursive.get() && (kind == ENTRY_CREATE)) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(child);
}
} catch (IOException x) {
System.err.println("Unable to register created directory for watching: " + child);
}
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
// if all directories are inaccessible
// even the root watch directory
// might wight want to cancel the service.
if (keys.isEmpty()) {
System.out.println("No directories being watched");
}
}
return Collections.unmodifiableList(
interestingEvents
);
}
}
}
为简单起见,JavaFX WatchDirService 的输入参数被编码为只读属性。这意味着如果您想将监视从递归更改为非递归或反之亦然,或者更改正在监视的目录,您将不得不取消现有服务并使用新设置创建一个新服务。可能可以让属性读写,以便可以修改现有正在运行的服务以查看不同的目录,但是让它工作对我来说似乎有点棘手,所以我没有尝试。
WatchService 和 JavaFX ScheduledService 的集成在实现方式、应用程序使用方式以及执行方式方面都表现出色,这让我感到非常惊喜。
目录监控WatchService的基本用法
以及一个演示其使用的示例应用(检测用户主目录根目录的变化):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
public class WatchDirApp extends Application {
private static final String WATCH_DIR = System.getProperty("user.home");
private WatchDirService watchDirService;
@Override
public void init() throws Exception {
watchDirService = new WatchDirService(
Paths.get(WATCH_DIR),
false
);
}
@Override
public void start(Stage stage) throws Exception {
ListView<String> events = new ListView<>();
watchDirService.start();
watchDirService.valueProperty().addListener((observable, previousEvents, newEvents) -> {
if (newEvents != null) {
newEvents.forEach(event ->
events.getItems().add(eventToString(event))
);
}
});
Scene scene = new Scene(new StackPane(events));
stage.setScene(scene);
stage.show();
}
private String eventToString(WatchEvent<Path> event) {
return event.kind() + ":" + event.context() + ":n=" + event.count();
}
@Override
public void stop() throws Exception {
watchDirService.cancel();
}
public static void main(String[] args) {
launch(args);
}
}
事件过滤示例
这是使用 JavaFX 的 WatchService 的更广泛示例,它将监视用户主目录中新文本文件的创建(或现有文件的修改)并启动一个新窗口以显示文件中的文本。
该示例演示了过滤监视服务生成的事件,以便可以根据事件采取不同的操作(例如在新窗口中查看文本文件或在修改文件时更新现有窗口的内容) .
在运行示例时,可以注意到在生成事件(例如保存新文本文件)和更新 UI(例如显示新保存的文本文件)之间存在一两秒的延迟)。这是因为监视服务不会实时通知更改事件(至少在我的运行 OS X 的机器上),而是稍微延迟通知更改事件。
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.WatchEvent;
import java.util.HashMap;
import java.util.Map;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
public class WatchedTextFileViewerApp extends Application {
private static final String TEXT_FILE_EXTENSION = ".txt";
private static final String WATCH_DIR = System.getProperty("user.home");
private WatchDirService watchDirService;
private Stage appStage;
private Map<Path, FileViewer> fileViewers = new HashMap<>();
@Override
public void init() throws Exception {
watchDirService = new WatchDirService(
Paths.get(WATCH_DIR),
false
);
}
@Override
public void start(Stage stage) throws Exception {
this.appStage = stage;
ListView<String> events = new ListView<>();
watchDirService.start();
watchDirService.valueProperty().addListener((observable, previousEvents, newEvents) -> {
if (newEvents != null) {
newEvents.forEach(event ->
events.getItems().add(eventToString(event))
);
newEvents.stream()
.filter(event -> ENTRY_CREATE.equals(event.kind()) && isForTextFile(event.context()))
.forEach(event -> view(event.context()));
newEvents.stream()
.filter(event -> ENTRY_MODIFY.equals(event.kind()) && isForTextFile(event.context()))
.forEach(event -> refresh(event.context()));
}
});
Scene scene = new Scene(new StackPane(events));
stage.setScene(scene);
stage.show();
}
@Override
public void stop() throws Exception {
watchDirService.cancel();
}
private boolean isForTextFile(Path path) {
return path != null
&& !Files.isDirectory(path)
&& path.toString().endsWith(TEXT_FILE_EXTENSION);
}
private FileViewer view(Path path) {
FileViewer fileViewer = new FileViewer(appStage, path);
fileViewers.put(path, fileViewer);
fileViewer.show();
return fileViewer;
}
private void refresh(Path path) {
FileViewer fileViewer = fileViewers.get(path);
if (fileViewer == null) {
fileViewer = view(path);
}
fileViewer.refreshText();
fileViewer.show();
}
private String eventToString(WatchEvent<Path> event) {
return event.kind() + ":" + event.context() + ":n=" + event.count();
}
private static class FileViewer extends Stage {
private TextArea textArea = new TextArea();
private Path path;
FileViewer(Stage owner, Path path) {
this.path = Paths.get(WATCH_DIR).resolve(path);
initOwner(owner);
setTitle(path.toString());
textArea.setEditable(false);
refreshText();
setScene(new Scene(textArea));
}
void refreshText() {
try {
textArea.setText(String.join("\n", Files.readAllLines(path)));
} catch (IOException e) {
System.err.println("Unable to read the content of: " + path);
}
}
}
public static void main(String[] args) {
launch(args);
}
}