diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java index 58d15551461f..13b9e39dc591 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -44,7 +45,7 @@ public class FileSystemWatcher { private static final long DEFAULT_QUIET_PERIOD = 400; - private List listeners = new ArrayList(); + private final List listeners = new ArrayList(); private final boolean daemon; @@ -52,14 +53,16 @@ public class FileSystemWatcher { private final long quietPeriod; - private Thread watchThread; + private final AtomicInteger remainingScans = new AtomicInteger(-1); - private AtomicInteger remainingScans = new AtomicInteger(-1); + private final Map folders = new HashMap(); - private Map folders = new LinkedHashMap(); + private Thread watchThread; private FileFilter triggerFilter; + private final Object monitor = new Object(); + /** * Create a new {@link FileSystemWatcher} instance. */ @@ -89,10 +92,12 @@ public FileSystemWatcher(boolean daemon, long pollInterval, long quietPeriod) { * {@link #start() started}. * @param fileChangeListener the listener to add */ - public synchronized void addListener(FileChangeListener fileChangeListener) { + public void addListener(FileChangeListener fileChangeListener) { Assert.notNull(fileChangeListener, "FileChangeListener must not be null"); - checkNotStarted(); - this.listeners.add(fileChangeListener); + synchronized (this.monitor) { + checkNotStarted(); + this.listeners.add(fileChangeListener); + } } /** @@ -102,8 +107,10 @@ public synchronized void addListener(FileChangeListener fileChangeListener) { */ public void addSourceFolders(Iterable folders) { Assert.notNull(folders, "Folders must not be null"); - for (File folder : folders) { - addSourceFolder(folder); + synchronized (this.monitor) { + for (File folder : folders) { + addSourceFolder(folder); + } } } @@ -112,54 +119,49 @@ public void addSourceFolders(Iterable folders) { * {@link #start() started}. * @param folder the folder to monitor */ - public synchronized void addSourceFolder(File folder) { + public void addSourceFolder(File folder) { Assert.notNull(folder, "Folder must not be null"); Assert.isTrue(folder.isDirectory(), "Folder '" + folder + "' must exist and must" + " be a directory"); - checkNotStarted(); - this.folders.put(folder, null); + synchronized (this.monitor) { + checkNotStarted(); + this.folders.put(folder, null); + } } /** * Set an optional {@link FileFilter} used to limit the files that trigger a change. * @param triggerFilter a trigger filter or null */ - public synchronized void setTriggerFilter(FileFilter triggerFilter) { - this.triggerFilter = triggerFilter; + public void setTriggerFilter(FileFilter triggerFilter) { + synchronized (this.monitor) { + this.triggerFilter = triggerFilter; + } } private void checkNotStarted() { - Assert.state(this.watchThread == null, "FileSystemWatcher already started"); + synchronized (this.monitor) { + Assert.state(this.watchThread == null, "FileSystemWatcher already started"); + } } /** * Start monitoring the source folder for changes. */ - public synchronized void start() { - saveInitialSnapshots(); - if (this.watchThread == null) { - this.watchThread = new Thread() { - @Override - public void run() { - int remaining = FileSystemWatcher.this.remainingScans.get(); - while (remaining > 0 || remaining == -1) { - try { - if (remaining > 0) { - FileSystemWatcher.this.remainingScans.decrementAndGet(); - } - scan(); - } - catch (InterruptedException ex) { - // Ignore - } - remaining = FileSystemWatcher.this.remainingScans.get(); - } - }; - }; - this.watchThread.setName("File Watcher"); - this.watchThread.setDaemon(this.daemon); - this.remainingScans = new AtomicInteger(-1); - this.watchThread.start(); + public void start() { + synchronized (this.monitor) { + saveInitialSnapshots(); + if (this.watchThread == null) { + Map localFolders = new HashMap(); + localFolders.putAll(this.folders); + this.watchThread = new Thread(new Watcher(this.remainingScans, + new ArrayList(this.listeners), + this.triggerFilter, this.pollInterval, this.quietPeriod, + localFolders)); + this.watchThread.setName("File Watcher"); + this.watchThread.setDaemon(this.daemon); + this.watchThread.start(); + } } } @@ -169,72 +171,10 @@ private void saveInitialSnapshots() { } } - private void scan() throws InterruptedException { - Thread.sleep(this.pollInterval - this.quietPeriod); - Map previous; - Map current = this.folders; - do { - previous = current; - current = getCurrentSnapshots(); - Thread.sleep(this.quietPeriod); - } - while (isDifferent(previous, current)); - if (isDifferent(this.folders, current)) { - updateSnapshots(current.values()); - } - } - - private boolean isDifferent(Map previous, - Map current) { - if (!previous.keySet().equals(current.keySet())) { - return true; - } - for (Map.Entry entry : previous.entrySet()) { - FolderSnapshot previousFolder = entry.getValue(); - FolderSnapshot currentFolder = current.get(entry.getKey()); - if (!previousFolder.equals(currentFolder, this.triggerFilter)) { - return true; - } - } - return false; - } - - private Map getCurrentSnapshots() { - Map snapshots = new LinkedHashMap(); - for (File folder : this.folders.keySet()) { - snapshots.put(folder, new FolderSnapshot(folder)); - } - return snapshots; - } - - private void updateSnapshots(Collection snapshots) { - Map updated = new LinkedHashMap(); - Set changeSet = new LinkedHashSet(); - for (FolderSnapshot snapshot : snapshots) { - FolderSnapshot previous = this.folders.get(snapshot.getFolder()); - updated.put(snapshot.getFolder(), snapshot); - ChangedFiles changedFiles = previous.getChangedFiles(snapshot, - this.triggerFilter); - if (!changedFiles.getFiles().isEmpty()) { - changeSet.add(changedFiles); - } - } - if (!changeSet.isEmpty()) { - fireListeners(Collections.unmodifiableSet(changeSet)); - } - this.folders = updated; - } - - private void fireListeners(Set changeSet) { - for (FileChangeListener listener : this.listeners) { - listener.onChange(changeSet); - } - } - /** * Stop monitoring the source folders. */ - public synchronized void stop() { + public void stop() { stopAfter(0); } @@ -242,23 +182,131 @@ public synchronized void stop() { * Stop monitoring the source folders. * @param remainingScans the number of remaining scans */ - synchronized void stopAfter(int remainingScans) { - Thread thread = this.watchThread; - if (thread != null) { - this.remainingScans.set(remainingScans); - if (remainingScans <= 0) { - thread.interrupt(); + void stopAfter(int remainingScans) { + synchronized (this.monitor) { + Thread thread = this.watchThread; + if (thread != null) { + this.remainingScans.set(remainingScans); + if (remainingScans <= 0) { + thread.interrupt(); + } + if (Thread.currentThread() != thread) { + try { + thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + this.watchThread = null; } - if (Thread.currentThread() != thread) { + } + } + + private static final class Watcher implements Runnable { + + private final AtomicInteger remainingScans; + + private final List listeners; + + private final FileFilter triggerFilter; + + private final long pollInterval; + + private final long quietPeriod; + + private Map folders; + + private Watcher(AtomicInteger remainingScans, List listeners, + FileFilter triggerFilter, long pollInterval, long quietPeriod, + Map folders) { + this.remainingScans = remainingScans; + this.listeners = listeners; + this.triggerFilter = triggerFilter; + this.pollInterval = pollInterval; + this.quietPeriod = quietPeriod; + this.folders = folders; + } + + @Override + public void run() { + int remainingScans = this.remainingScans.get(); + while (remainingScans > 0 || remainingScans == -1) { try { - thread.join(); + if (remainingScans > 0) { + this.remainingScans.decrementAndGet(); + } + scan(); } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); + // Ignore } + remainingScans = this.remainingScans.get(); + } + }; + + private void scan() throws InterruptedException { + Thread.sleep(this.pollInterval - this.quietPeriod); + Map previous; + Map current = this.folders; + do { + previous = current; + current = getCurrentSnapshots(); + Thread.sleep(this.quietPeriod); + } + while (isDifferent(previous, current)); + if (isDifferent(this.folders, current)) { + updateSnapshots(current.values()); } - this.watchThread = null; } + + private boolean isDifferent(Map previous, + Map current) { + if (!previous.keySet().equals(current.keySet())) { + return true; + } + for (Map.Entry entry : previous.entrySet()) { + FolderSnapshot previousFolder = entry.getValue(); + FolderSnapshot currentFolder = current.get(entry.getKey()); + if (!previousFolder.equals(currentFolder, this.triggerFilter)) { + return true; + } + } + return false; + } + + private Map getCurrentSnapshots() { + Map snapshots = new LinkedHashMap(); + for (File folder : this.folders.keySet()) { + snapshots.put(folder, new FolderSnapshot(folder)); + } + return snapshots; + } + + private void updateSnapshots(Collection snapshots) { + Map updated = new LinkedHashMap(); + Set changeSet = new LinkedHashSet(); + for (FolderSnapshot snapshot : snapshots) { + FolderSnapshot previous = this.folders.get(snapshot.getFolder()); + updated.put(snapshot.getFolder(), snapshot); + ChangedFiles changedFiles = previous.getChangedFiles(snapshot, + this.triggerFilter); + if (!changedFiles.getFiles().isEmpty()) { + changeSet.add(changedFiles); + } + } + if (!changeSet.isEmpty()) { + fireListeners(Collections.unmodifiableSet(changeSet)); + } + this.folders = updated; + } + + private void fireListeners(Set changeSet) { + for (FileChangeListener listener : this.listeners) { + listener.onChange(changeSet); + } + } + } }