很多有效的业务系统中,采用了传说中万能的dirwatch解决方案,所以讨论在java下的对目录下的文件监控是挺有意义的事情。
WatchService里面提供了对文件夹监控的标准接口WatchService,但是这个接口只提供了Delete,Modify和Create三种事件的监控。
之前,我们一直使用的JNotify,也是只提供了这几个接口事件。
如果我们在Java的网站中,对用户刚刚上传完毕的文件进行立即处理的情况下, 应对对文件的完整性进行确认。
WatchService里面提供给我们的三个事件,是否能让我们判断这个文件是否完整呢?
我写了一个测试用例,代码如下:
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package fswatch;
import com.kamike.message.FsWatchService;
/**
*
* @author THiNk
*/
public class FsWatch {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic here
FsWatchService.start("D:\\temp\\");
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PathEvents {
private final List<PathEvent> pathEvents = new ArrayList<PathEvent>();
private final Path watchedDirectory;
private final boolean isValid;
PathEvents(boolean valid, Path watchedDirectory) {
isValid = valid;
this.watchedDirectory = watchedDirectory;
}
public boolean isValid(){
return isValid;
}
public Path getWatchedDirectory(){
return watchedDirectory;
}
public List<PathEvent> getEvents() {
return Collections.unmodifiableList(pathEvents);
}
public void add(PathEvent pathEvent) {
pathEvents.add(pathEvent);
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
/**
*
* @author THiNk
*/
public class PathEvent {
private final Path eventTarget;
private final WatchEvent.Kind type;
PathEvent(Path eventTarget, WatchEvent.Kind type) {
this.eventTarget = eventTarget;
this.type = type;
}
public Path getEventTarget() {
return eventTarget;
}
public WatchEvent.Kind getType() {
return type;
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import com.google.common.base.Function;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
/**
*
* @author THiNk
*/
public class FunctionVisitor extends SimpleFileVisitor<Path> {
Function<Path,FileVisitResult> pathFunction;
public FunctionVisitor(Function<Path, FileVisitResult> pathFunction) {
this.pathFunction = pathFunction;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
return pathFunction.apply(file);
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import com.google.common.eventbus.EventBus;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author THiNk
*/
public class FsWatcher {
private FutureTask<Integer> watchTask;
private EventBus eventBus;
private WatchService watchService;
private volatile boolean keepWatching = true;
private Path startPath;
public FsWatcher(EventBus eventBus, Path startPath) {
this.eventBus = Objects.requireNonNull(eventBus);
this.startPath = Objects.requireNonNull(startPath);
}
public void start() throws IOException {
initWatchService();
registerDirectories();
createWatchTask();
startWatching();
}
public boolean isRunning() {
return watchTask != null && !watchTask.isDone();
}
public void stop() {
keepWatching = false;
}
public void close()
{
try {
this.stop();
this.watchService.close();
} catch (IOException ex) {
Logger.getLogger(FsWatcher.class.getName()).log(Level.SEVERE, null, ex);
}
}
//Used for testing purposes
Integer getEventCount() {
try {
return watchTask.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException ex) {
throw new RuntimeException(ex);
}
}
private void createWatchTask() {
watchTask = new FutureTask<Integer>(new Callable<Integer>() {
private int totalEventCount;
@Override
public Integer call() throws Exception {
while (keepWatching) {
WatchKey watchKey = watchService.poll(10, TimeUnit.SECONDS);
if (watchKey != null) {
List<WatchEvent<?>> events = watchKey.pollEvents();
Path watched = (Path) watchKey.watchable();
// PathEvents pathEvents = new PathEvents(watchKey.isValid(), watched);
// for (WatchEvent event : events) {
// pathEvents.add(new PathEvent((Path) event.context(), event.kind()));
// totalEventCount++;
// }
// watchKey.reset();
//
for(WatchEvent event : events)
{
PathEvent pathEvent= new PathEvent((Path) event.context(), event.kind());
eventBus.post(pathEvent);
}
watchKey.reset();
}
}
return totalEventCount;
}
});
}
private void startWatching() {
new Thread(watchTask).start();
}
private void registerDirectories() throws IOException {
Files.walkFileTree(startPath, new WatchServiceRegisteringVisitor());
}
private WatchService initWatchService() throws IOException {
if (watchService == null) {
watchService = FileSystems.getDefault().newWatchService();
}
return watchService;
}
private class WatchServiceRegisteringVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
//dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
return FileVisitResult.CONTINUE;
}
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
*
* @author THiNk
*/
public class FileDirectoryStream {
File startDirectory;
String pattern;
private LinkedBlockingQueue<File> fileLinkedBlockingQueue = new LinkedBlockingQueue<File>();
private boolean closed = false;
private FutureTask<Void> fileTask;
private FilenameFilter filenameFilter;
public FileDirectoryStream(String pattern, File startDirectory) {
this.pattern = pattern;
this.startDirectory = startDirectory;
this.filenameFilter = getFileNameFilter(pattern);
}
public Iterator<File> glob() throws IOException {
confirmNotClosed();
startFileSearch(startDirectory, filenameFilter);
return new Iterator<File>() {
File file = null;
@Override
public boolean hasNext() {
try {
file = fileLinkedBlockingQueue.poll();
while (!fileTask.isDone() && file == null) {
file = fileLinkedBlockingQueue.poll(5, TimeUnit.MILLISECONDS);
}
return file != null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
@Override
public File next() {
return file;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
};
}
private void startFileSearch(final File startDirectory, final FilenameFilter filenameFilter) {
fileTask = new FutureTask<Void>(new Callable<Void>() {
@Override
public Void call() throws Exception {
findFiles(startDirectory, filenameFilter);
return null;
}
});
start(fileTask);
}
private void findFiles(final File startDirectory, final FilenameFilter filenameFilter) {
File[] files = startDirectory.listFiles(filenameFilter);
for (File file : files) {
if (!fileTask.isCancelled()) {
if (file.isDirectory()) {
findFiles(file, filenameFilter);
}
fileLinkedBlockingQueue.offer(file);
}
}
}
private FilenameFilter getFileNameFilter(final String pattern) {
return new FilenameFilter() {
Pattern regexPattern = Pattern.compile(pattern);
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isDirectory() || regexPattern.matcher(name).matches();
}
};
}
public void close() throws IOException {
if (fileTask != null) {
fileTask.cancel(true);
}
fileLinkedBlockingQueue.clear();
fileLinkedBlockingQueue = null;
fileTask = null;
closed = true;
}
private void start(FutureTask<Void> futureTask) {
new Thread(futureTask).start();
}
private void confirmNotClosed() {
if (closed) {
throw new IllegalStateException("File Iterator has already been closed");
}
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message.fswatch;
import com.google.common.base.Function;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
*
* @author THiNk
*/
public class AsynchronousRecursiveDirectoryStream implements DirectoryStream<Path> {
private LinkedBlockingQueue<Path> pathsBlockingQueue = new LinkedBlockingQueue<Path>();
private boolean closed = false;
private FutureTask<Void> pathTask;
private Path startPath;
private Filter filter;
public AsynchronousRecursiveDirectoryStream(Path startPath, String pattern) throws IOException {
this.startPath = Objects.requireNonNull(startPath);
}
@Override
public Iterator<Path> iterator() {
confirmNotClosed();
findFiles(startPath, filter);
return new Iterator<Path>() {
Path path;
@Override
public boolean hasNext() {
try {
path = pathsBlockingQueue.poll();
while (!pathTask.isDone() && path == null) {
path = pathsBlockingQueue.poll(5, TimeUnit.MILLISECONDS);
}
return (path != null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
@Override
public Path next() {
return path;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Removal not supported");
}
};
}
private void findFiles(final Path startPath, final Filter filter) {
pathTask = new FutureTask<Void>(new Callable<Void>() {
@Override
public Void call() throws Exception {
Files.walkFileTree(startPath, new FunctionVisitor(getFunction(filter)));
return null;
}
});
start(pathTask);
}
private Function<Path, FileVisitResult> getFunction(final Filter<Path> filter) {
return new Function<Path, FileVisitResult>() {
@Override
public FileVisitResult apply(Path input) {
try {
if (filter.accept(input.getFileName())) {
pathsBlockingQueue.offer(input);
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
return (pathTask.isCancelled()) ? FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
}
};
}
@Override
public void close() throws IOException {
if(pathTask !=null){
pathTask.cancel(true);
}
pathsBlockingQueue.clear();
pathsBlockingQueue = null;
pathTask = null;
filter = null;
closed = true;
}
private void start(FutureTask<Void> futureTask) {
new Thread(futureTask).start();
}
private void confirmNotClosed() {
if (closed) {
throw new IllegalStateException("DirectoryStream has already been closed");
}
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.kamike.message.fswatch.FsWatcher;
import com.kamike.message.fswatch.PathEvent;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FilenameUtils;
/**
*
* @author THiNk
*/
public class FsWatchService {
private static FsWatcher fsw;
private static volatile ReentrantLock lock = new ReentrantLock();
public FsWatchService()
{
}
@Subscribe
@AllowConcurrentEvents
public void proc(PathEvent event)
{
try {
Path path = event.getEventTarget();
String fileName = FilenameUtils.concat("D:\\temp\\", path.toString());
if (fileName.endsWith(".aspx")) {
String fullPath = FilenameUtils.getFullPath(fileName);
String srcName = FilenameUtils.getBaseName(fileName);
}
} catch (Error e) {
e.printStackTrace();
}
}
public static void start(String path) {
if (fsw == null) {
lock.lock();
if (fsw == null) {
try {
fsw = new FsWatcher(EventInst.getInstance().getAsyncEventBus(),
Paths.get(path));
try {
fsw.start();
} catch (IOException ex) {
Logger.getLogger(FsWatchService.class.getName()).log(Level.SEVERE, null, ex);
}
} finally {
lock.unlock();
}
}
}
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.kamike.message;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import java.util.concurrent.Executors;
/**
*
* @author THiNk
*/
public class EventInst {
private static volatile EventInst eventInst = new EventInst();
private EventBus eventBus;
private AsyncEventBus asyncEventBus;
private FsWatchService fs=new FsWatchService();
private EventInst() {
eventBus=new EventBus();
asyncEventBus = new AsyncEventBus(Executors.newFixedThreadPool(50));
asyncEventBus.register(fs);
}
public static EventInst getInstance() {
return eventInst;
}
/**
* @return the eventBus
*/
public EventBus getEventBus() {
return eventBus;
}
/**
* @return the eventBus
*/
public AsyncEventBus getAsyncEventBus() {
return asyncEventBus;
}
}
通过对上面的代码的测试发现:
在Http的文件上传和文件正常拷贝过程中,会触发一次create,一次紧接着create的modify,然后一直传输,最后结束时的触发一次modify.
在真实的业务场景里面,Http的上传,往往文件非常小,最多几M,如果出现中断,用户一般会重传,所以采用记录create,modify,然后等待第二次modify的方法,可以确切的在文件传输完毕后,对第二次modify进行响应,及时进行处理。
而在ftp上传过程中,如果出现了传输中断,或者是文件拷贝过程中被用户终止,在发送这种中断时,也会触发一次modify,此时并不能判断文件是否传完。
在这种情况下,采用对第二次modify进行响应的做法,并不能准确起作用。
但是,某些断点续传功能的ftp server和传输软件(比如迅雷)会在未传输成功的文件的同一个目录下,建立.cfg文件,记录文件的传输进度,当传输成功后,这个.cfg会被删除,这种情况下,可以对这个.cfg的delete事件进行响应。