Tôi đã tạo một trình bao bọc xung quanh WatchService để dọn sạch nhiều vấn đề tôi có với API. Nó bây giờ là nhiều hơn nữa testable. Tôi không chắc chắn về một số vấn đề tương tranh trong PathWatchService mặc dù và tôi đã không thực hiện kiểm tra kỹ lưỡng của nó.
New FileWatcher:
public class FileWatcher {
private final PathWatchService pathWatchService;
private final Path path;
private String data;
public FileWatcher(PathWatchService pathWatchService, Path path) {
this.path = path;
this.pathWatchService = pathWatchService;
try {
this.pathWatchService.register(path.toAbsolutePath().getParent());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
load();
}
private void load() {
try (BufferedReader br = Files.newBufferedReader(path, Charset.defaultCharset())){
data = br.readLine();
} catch (IOException ex) {
data = "";
}
}
public void update(){
PathEvents pe;
while ((pe=pathWatchService.poll()) != null) {
for (WatchEvent we : pe.getEvents()){
if (path.equals(we.context())){
load();
return;
}
}
}
}
public String getData(){
update();
return data;
}
}
Wrapper:
public class PathWatchService implements AutoCloseable {
private final WatchService watchService;
private final BiMap<WatchKey, Path> watchKeyToPath = HashBiMap.create();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Queue<WatchKey> invalidKeys = new ConcurrentLinkedQueue<>();
/**
* Constructor.
*/
public PathWatchService() {
try {
watchService = FileSystems.getDefault().newWatchService();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Register the input path with the WatchService for all
* StandardWatchEventKinds. Registering a path which is already being
* watched has no effect.
*
* @param path
* @return
* @throws IOException
*/
public void register(Path path) throws IOException {
register(path, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
/**
* Register the input path with the WatchService for the input event kinds.
* Registering a path which is already being watched has no effect.
*
* @param path
* @param kinds
* @return
* @throws IOException
*/
public void register(Path path, WatchEvent.Kind... kinds) throws IOException {
try {
lock.writeLock().lock();
removeInvalidKeys();
WatchKey key = watchKeyToPath.inverse().get(path);
if (key == null) {
key = path.register(watchService, kinds);
watchKeyToPath.put(key, path);
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Close the WatchService.
*
* @throws IOException
*/
@Override
public void close() throws IOException {
try {
lock.writeLock().lock();
watchService.close();
watchKeyToPath.clear();
invalidKeys.clear();
} finally {
lock.writeLock().unlock();
}
}
/**
* Retrieves and removes the next PathEvents object, or returns null if none
* are present.
*
* @return
*/
public PathEvents poll() {
return keyToPathEvents(watchService.poll());
}
/**
* Return a PathEvents object from the input key.
*
* @param key
* @return
*/
private PathEvents keyToPathEvents(WatchKey key) {
if (key == null) {
return null;
}
try {
lock.readLock().lock();
Path watched = watchKeyToPath.get(key);
List<WatchEvent<Path>> events = new ArrayList<>();
for (WatchEvent e : key.pollEvents()) {
events.add((WatchEvent<Path>) e);
}
boolean isValid = key.reset();
if (isValid == false) {
invalidKeys.add(key);
}
return new PathEvents(watched, events, isValid);
} finally {
lock.readLock().unlock();
}
}
/**
* Retrieves and removes the next PathEvents object, waiting if necessary up
* to the specified wait time, returns null if none are present after the
* specified wait time.
*
* @return
*/
public PathEvents poll(long timeout, TimeUnit unit) throws InterruptedException {
return keyToPathEvents(watchService.poll(timeout, unit));
}
/**
* Retrieves and removes the next PathEvents object, waiting if none are yet
* present.
*
* @return
*/
public PathEvents take() throws InterruptedException {
return keyToPathEvents(watchService.take());
}
/**
* Get all paths currently being watched. Any paths which were watched but
* have invalid keys are not returned.
*
* @return
*/
public Set<Path> getWatchedPaths() {
try {
lock.readLock().lock();
Set<Path> paths = new HashSet<>(watchKeyToPath.inverse().keySet());
WatchKey key;
while ((key = invalidKeys.poll()) != null) {
paths.remove(watchKeyToPath.get(key));
}
return paths;
} finally {
lock.readLock().unlock();
}
}
/**
* Cancel watching the specified path. Cancelling a path which is not being
* watched has no effect.
*
* @param path
*/
public void cancel(Path path) {
try {
lock.writeLock().lock();
removeInvalidKeys();
WatchKey key = watchKeyToPath.inverse().remove(path);
if (key != null) {
key.cancel();
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Removes any invalid keys from internal data structures. Note this
* operation is also performed during register and cancel calls.
*/
public void cleanUp() {
try {
lock.writeLock().lock();
removeInvalidKeys();
} finally {
lock.writeLock().unlock();
}
}
/**
* Clean up method to remove invalid keys, must be called from inside an
* acquired write lock.
*/
private void removeInvalidKeys() {
WatchKey key;
while ((key = invalidKeys.poll()) != null) {
watchKeyToPath.remove(key);
}
}
}
lớp dữ liệu:
public class PathEvents {
private final Path watched;
private final ImmutableList<WatchEvent<Path>> events;
private final boolean isValid;
/**
* Constructor.
*
* @param watched
* @param events
* @param isValid
*/
public PathEvents(Path watched, List<WatchEvent<Path>> events, boolean isValid) {
this.watched = watched;
this.events = ImmutableList.copyOf(events);
this.isValid = isValid;
}
/**
* Return an immutable list of WatchEvent's.
* @return
*/
public List<WatchEvent<Path>> getEvents() {
return events;
}
/**
* True if the watched path is valid.
* @return
*/
public boolean isIsValid() {
return isValid;
}
/**
* Return the path being watched in which these events occurred.
*
* @return
*/
public Path getWatched() {
return watched;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PathEvents other = (PathEvents) obj;
if (!Objects.equals(this.watched, other.watched)) {
return false;
}
if (!Objects.equals(this.events, other.events)) {
return false;
}
if (this.isValid != other.isValid) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 71 * hash + Objects.hashCode(this.watched);
hash = 71 * hash + Objects.hashCode(this.events);
hash = 71 * hash + (this.isValid ? 1 : 0);
return hash;
}
@Override
public String toString() {
return "PathEvents{" + "watched=" + watched + ", events=" + events + ", isValid=" + isValid + '}';
}
}
Và cuối cùng là kiểm tra, lưu ý đây không phải là một thử nghiệm đơn vị hoàn chỉnh nhưng chứng tỏ đường đi để viết các bài kiểm tra cho tình huống này.
public class FileWatcherTest {
public FileWatcherTest() {
}
Path path = Paths.get("myFile.txt");
Path parent = path.toAbsolutePath().getParent();
private void write(String s) throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.defaultCharset())) {
bw.write(s);
}
}
@Test
public void test() throws IOException, InterruptedException{
write("hello");
PathWatchService real = new PathWatchService();
real.register(parent);
PathWatchService mock = mock(PathWatchService.class);
FileWatcher fileWatcher = new FileWatcher(mock, path);
verify(mock).register(parent);
Assert.assertEquals("hello", fileWatcher.getData());
write("goodbye");
PathEvents pe = real.poll(10, TimeUnit.SECONDS);
if (pe == null){
Assert.fail("Should have an event for writing good bye");
}
when(mock.poll()).thenReturn(pe).thenReturn(null);
Assert.assertEquals("goodbye", fileWatcher.getData());
}
}
Về bit "có mùi" - tất cả những gì tôi có thể nói là "cố gắng tránh các cuộc gọi tĩnh" !!- Bạn luôn có thể sử dụng 'PowerMock' (mà tôi cố gắng tránh trừ khi hoàn toàn cần thiết) – BretC
Có thể kiểm tra đơn vị là từ sai. Về cơ bản tôi muốn thử nghiệm nó, bao gồm cả sự tương tác với hệ thống tập tin. Đây là một ví dụ rất đơn giản, nhưng việc sử dụng thực sự phức tạp hơn một chút. Vấn đề chính của tôi là path.register cần một phương pháp riêng tư ma thuật không có giấy tờ để làm việc, điều này làm cho việc chế nhạo thậm chí còn khó khăn hơn. Các chức năng của WatchService là tuyệt vời nhưng API là khủng khiếp, nhắc nhở tôi về mã di sản xấu xí, không phải java cơ sở gần đây. Tôi muốn thử một vài điều, và nếu tôi không thể có được bất cứ điều gì tốt hơn tôi sẽ chấp nhận câu trả lời này và chỉ ngủ trong bài kiểm tra. – user2133814