バックグラウンド
ルールエンジンを検討する際に、ルールがファイル形式で保存されている場合は、指定したディレクトリまたはファイルを監視して、ルールが変更されたかどうかを確認してからロードする必要があります。もちろん、構成ファイルの動的ロード、ログファイルの監視、FTPファイルの変更の監視など、他のビジネスシナリオでも、同様のシナリオが発生します。
この記事では、3つのソリューションを提供し、長所と短所を分析し、緊急時のコレクションを推奨します。
オプション1:スケジュールされたタスク+ File#lastModified
このソリューションは、頭に浮かぶ最も単純で最も簡単なソリューションです。時限タスクを通じて、クエリファイルの最終変更時刻がポーリングされ、前回と比較されます。変更がある場合は、ファイルが変更されており、対応するビジネスロジックでリロードまたは処理する必要があることを意味します。
前回の記事「JDKのバグ、ファイルの変更を監視するときは注意してください」では、具体的な例が書かれており、その欠点も指摘されています。
ここにサンプルコードを貼り付けます。
public class FileWatchDemo {
/**
* 上次更新时间
*/
public static long LAST_TIME = 0L;
public static void main(String[] args) throws IOException {
String fileName = "/Users/zzs/temp/1.txt";
// 创建文件,仅为实例,实践中由其他程序触发文件的变更
createFile(fileName);
// 执行2次
for (int i = 0; i < 2; i++) {
long timestamp = readLastModified(fileName);
if (timestamp != LAST_TIME) {
System.out.println("文件已被更新:" + timestamp);
LAST_TIME = timestamp;
// 重新加载,文件内容
} else {
System.out.println("文件未更新");
}
}
}
public static void createFile(String fileName) throws IOException {
File file = new File(fileName);
if (!file.exists()) {
boolean result = file.createNewFile();
System.out.println("创建文件:" + result);
}
}
public static long readLastModified(String fileName) {
File file = new File(fileName);
return file.lastModified();
}
}
复制代码
低頻度のファイル変更を伴うシナリオの場合、このソリューションは実装が簡単で、基本的に要件を満たしています。ただし、前の記事で述べたように、Java8およびJava9でのFile#lastModifiedのバグに注意する必要があります。
ただし、このスキームをファイルディレクトリの変更に使用すると、欠点がいくらか明らかになります。たとえば、操作が頻繁に行われ、トラバース、状態の保存、状態の比較の効率が低下し、OSの機能が失われます。十分に活用されます。
オプション2:WatchService
Java 7で追加され、java.nio.file.WatchService
ファイルの変更を監視できます。WatchServiceは、オペレーティングシステムに基づくファイルシステムモニターであり、トラバースや比較を行わずにシステム内のすべてのファイルの変更を監視できます。信号の送受信に基づく監視であり、効率が高くなります。
public class WatchServiceDemo {
public static void main(String[] args) throws IOException {
// 这里的监听必须是目录
Path path = Paths.get("/Users/zzs/temp/");
// 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多
WatchService watcher = FileSystems.getDefault().newWatchService();
// 注册指定目录使用的监听器,监视目录下文件的变化;
// PS:Path必须是目录,不能是文件;
// StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件
path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
// 创建一个线程,等待目录下的文件发生变化
try {
while (true) {
// 获取目录的变化:
// take()是一个阻塞方法,会等待监视器发出的信号才返回。
// 还可以使用watcher.poll()方法,非阻塞方法,会立即返回当时监视器中是否有信号。
// 返回结果WatchKey,是一个单例对象,与前面的register方法返回的实例是同一个;
WatchKey key = watcher.take();
// 处理文件变化事件:
// key.pollEvents()用于获取文件变化事件,只能获取一次,不能重复获取,类似队列的形式。
for (WatchEvent<?> event : key.pollEvents()) {
// event.kind():事件类型
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
//事件可能lost or discarded
continue;
}
// 返回触发事件的文件或目录的路径(相对路径)
Path fileName = (Path) event.context();
System.out.println("文件更新: " + fileName);
}
// 每次调用WatchService的take()或poll()方法时需要通过本方法重置
if (!key.reset()) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
上記のデモはWatchServiceの基本的な使用法を示しており、アノテーション部分では各APIの特定の役割についても説明しています。
WatchServiceによって監視されるファイルの種類も豊富になりました。
- ENTRY_CREATEターゲットが作成されます
- ENTRY_DELETEターゲットが削除されました
- ENTRY_MODIFYターゲットが変更されました
- オーバーフローイベントが放棄または失われたことを示す特別なイベント
如果查看WatchService实现类(PollingWatchService)的源码,会发现,本质上就是开启了一个独立的线程来监控文件的变化:
PollingWatchService() {
// TBD: Make the number of threads configurable
scheduledExecutor = Executors
.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);
t.setDaemon(true);
return t;
}});
}
复制代码
也就是说,本来需要我们手动实现的部分,也由WatchService内部帮我们完成了。
如果你编写一个demo,进行验证时,会很明显的感觉到WatchService监控文件的变化并不是实时的,有时候要等几秒才监听到文件的变化。以实现类PollingWatchService为例,查看源码,可以看到如下代码:
void enable(Set<? extends Kind<?>> var1, long var2) {
synchronized(this) {
this.events = var1;
Runnable var5 = new Runnable() {
public void run() {
PollingWatchKey.this.poll();
}
};
this.poller = PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5, var2, var2, TimeUnit.SECONDS);
}
}
复制代码
也就是说监听器由按照固定时间间隔的调度器来控制的,而这个时间间隔在SensitivityWatchEventModifier类中定义:
public enum SensitivityWatchEventModifier implements Modifier {
HIGH(2),
MEDIUM(10),
LOW(30);
// ...
}
复制代码
该类提供了3个级别的时间间隔,分别为2秒、10秒、30秒,默认值为10秒。这个时间间隔可以在path#register时进行传递:
path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},
SensitivityWatchEventModifier.HIGH);
复制代码
相对于方案一,实现起来简单,效率高。不足的地方也很明显,只能监听当前目录下的文件和目录,不能监视子目录,而且我们也看到监听只能算是准实时的,而且监听时间只能取API默认提供的三个值。
该API在Stack Overflow上也有人提出Java 7在Mac OS下有延迟的问题,甚至涉及到Windows和Linux系统,笔者没有进行其他操作系统的验证,如果你遇到类似的问题,可参考对应的文章,寻求解决方案:blog.csdn.net/claram/arti… 。
方案三:Apache Commons-IO
方案一我们自己来实现,方案二借助于JDK的API来实现,方案三便是借助于开源的框架来实现,这就是几乎每个项目都会引入的commons-io类库。
引入相应依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
复制代码
注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本。
commons-io对实现文件监听的实现位于org.apache.commons.io.monitor包下,基本使用流程如下:
- ファイル監視クラスをカスタマイズし、
FileAlterationListenerAdaptor
それ、ファイルとディレクトリの作成、変更、および削除を処理します。 - カスタムファイル監視クラス、ディレクトリを指定してオブザーバーを作成します
FileAlterationObserver
; - ファイルシステムウォッチャーをモニターに追加し、ファイルリスナーを追加します。
- 呼び出して実行します。
手順1:ファイルリスナーを作成します。必要に応じて、対応するビジネスロジック処理をさまざまな方法で実装します。
public class FileListener extends FileAlterationListenerAdaptor {
@Override
public void onStart(FileAlterationObserver observer) {
super.onStart(observer);
System.out.println("onStart");
}
@Override
public void onDirectoryCreate(File directory) {
System.out.println("新建:" + directory.getAbsolutePath());
}
@Override
public void onDirectoryChange(File directory) {
System.out.println("修改:" + directory.getAbsolutePath());
}
@Override
public void onDirectoryDelete(File directory) {
System.out.println("删除:" + directory.getAbsolutePath());
}
@Override
public void onFileCreate(File file) {
String compressedPath = file.getAbsolutePath();
System.out.println("新建:" + compressedPath);
if (file.canRead()) {
// TODO 读取或重新加载文件内容
System.out.println("文件变更,进行处理");
}
}
@Override
public void onFileChange(File file) {
String compressedPath = file.getAbsolutePath();
System.out.println("修改:" + compressedPath);
}
@Override
public void onFileDelete(File file) {
System.out.println("删除:" + file.getAbsolutePath());
}
@Override
public void onStop(FileAlterationObserver observer) {
super.onStop(observer);
System.out.println("onStop");
}
}
复制代码
手順2:ファイル監視用のツールクラスをカプセル化するコアは、オブザーバーFileAlterationObserverを作成し、ファイルパスPathとリスナーFileAlterationListenerをカプセル化してから、FileAlterationMonitorに渡すことです。
public class FileMonitor {
private FileAlterationMonitor monitor;
public FileMonitor(long interval) {
monitor = new FileAlterationMonitor(interval);
}
/**
* 给文件添加监听
*
* @param path 文件路径
* @param listener 文件监听器
*/
public void monitor(String path, FileAlterationListener listener) {
FileAlterationObserver observer = new FileAlterationObserver(new File(path));
monitor.addObserver(observer);
observer.addListener(listener);
}
public void stop() throws Exception {
monitor.stop();
}
public void start() throws Exception {
monitor.start();
}
}
复制代码
ステップ3:呼び出して実行します:
public class FileRunner {
public static void main(String[] args) throws Exception {
FileMonitor fileMonitor = new FileMonitor(1000);
fileMonitor.monitor("/Users/zzs/temp/", new FileListener());
fileMonitor.start();
}
}
复制代码
プログラムを実行すると、ログが1秒ごとに入力されていることがわかります。ファイルが変更されると、対応するログも出力されます。
onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop
复制代码
もちろん、FileMonitorの作成時に、対応するリスニング時間間隔を変更できます。
このスキームでは、リスナー自体がタイミング処理のためのスレッドを開始します。実行するたびに、イベントリスナー処理クラスのonStartメソッドが最初に呼び出され、次に変更があるかどうかを確認し、イベントに対応するメソッドを呼び出します。たとえば、onChangeファイルの内容が変更された場合は、確認後にonStopメソッドを呼び出して、現在スレッドが占有しているCPUリソースを解放し、次の時間間隔が再びウェイクアップして実行されるのを待ちます。
リスナーはルートとしてのファイルディレクトリに基づいており、対応するファイルの変更を監視するためのフィルターを設定することもできます。フィルタ設定は、FileAlterationObserverのコンストラクタで表示できます。
public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
this(new File(directoryName), fileFilter, caseSensitivity);
}
复制代码
まとめ
これまでに、Javaに基づいてファイルの変更を監視するための3つのスキームが導入されました。上記の分析と例の後で、完璧なソリューションはないことがわかりました。ビジネスの状況とシステムの許容範囲に応じて、最適なソリューションを選択できます。さらに、特定のスキームの欠陥を回避するために、これに基づいて他のいくつかの補助的な手段を追加することができます。
ブロガープロフィール:技術を学び、技術記事を書くのが大好きな技術書「SpringBootTechnologyInsider」の著者。
パブリックアカウント:ブロガーのパブリックアカウントである「ProgramNewVision」、注目を集めてください〜
技術交流:ブロガーに連絡してくださいWeChat ID:zhuan2quan