使用Akka actor进行反应式文件系统监视

在本文中,我们将讨论:

  1. 使用Java NIO.2监视文件系统
  2. 默认Java库的常见陷阱
  3. 设计一个简单的基于线程的文件系统监视器
  4. 使用以上内容,使用actor 模型设计一个反应式文件系统监视器

注意 :尽管这里所有代码示例都在Scala中,但是也可以用简单的Java重写。 为了快速熟悉Scala语法, 这是一个非常简短的Scala备忘单 。 有关面向Java程序员的Scala的更全面指南, 请查阅此文章(无需关注本文)。

对于绝对最简短的备忘单,请使用以下Java代码:

public void foo(int x, int y) {
  int z = x + y
  if (z == 1) {
    System.out.println(x);
  } else {
    System.out.println(y);
  }
}

等效于以下Scala代码:

def foo(x: Int, y: Int): Unit = {
  val z: Int = x + y
  z match {
   case 1 => println(x)
   case _ => println(y)
  }
}

此处提供的所有代码均已获得MIT许可,并作为GitHub上的Better -Files库的一部分提供。

假设您的任务是构建一个跨平台的桌面文件搜索引擎。 您很快意识到,在对所有文件进行初始索引编制之后,您还需要快速为创建或更新的所有新文件(或目录)重新编制索引。 天真的方法是每隔几分钟重新扫描整个文件系统。 但由于大多数操作系统暴露文件系统通知API,允许应用程序员更改,例如注册回调,这将是令人难以置信的低效率ionotify在Linux中, FSEvenets在Mac和FindFirstChangeNotification在Windows中。

但是,现在您不得不处理操作系统特定的API! 幸运的是,从Java SE 7开始,我们有了一个独立于平台的抽象,用于通过WatchService API监视文件系统的更改。 WatchService API是Java NIO.2的一部分,是在JSR-51下开发的,下面是一个“ hello world”示例,使用它来监视给定的Path

import java.nio.file._
import java.nio.file.StandardWatchEventKinds._
import scala.collection.JavaConversions._

def watch(directory: Path): Unit = {
  // First create the service
  val service: WatchService = directory.getFileSystem.newWatchService()

  // Register the service to the path and also specify which events we want to be notified about
  directory.register(service,  ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)

  while (true) {
    val key: WatchKey = service.take()  // Wait for this key to be signalled
    for {event <- key.pollEvents()} {
      // event.context() is the path to the file that got changed  
      event.kind() match {
        case ENTRY_CREATE => println(s"${event.context()} got created")
        case ENTRY_MODIFY => println(s"${event.context()} got modified")
        case ENTRY_DELETE => println(s"${event.context()} got deleted")        
        case _ => 
          // This can happen when OS discards or loses an event. 
          // See: http://docs.oracle.com/javase/8/docs/api/java/nio/file/StandardWatchEventKinds.html#OVERFLOW
          println(s"Unknown event $event happened at ${event.context()}")
      }
    }
    key.reset()  // Do not forget to do this!! See: http://stackoverflow.com/questions/20180547/
  }
}

尽管以上是一个很好的首次尝试,但它缺少几个方面:

  1. 错误的设计 :上面的代码看起来不自然,您可能必须在StackOverflow查找才能正确使用。 我们可以做得更好吗?
  2. 错误的设计 :该代码不能很好地处理错误。 当我们遇到无法打开的文件时会发生什么?
  3. 陷阱 :Java API仅允许我们监视目录中对其直接子级的更改。 它不会递归地为您查看目录
  4. 注意 :Java API 不允许我们观看单个文件 ,只能观看目录。
  5. 陷阱 :即使我们解决了上述问题,Java API 也不会自动开始监视在根目录下创建的新子文件或目录。
  6. 错误的设计 :上面实现的代码公开了基于线程的阻塞/轮询模型。 我们可以使用更好的并发抽象吗?

让我们从上述每个问题开始。

  • 更好的界面 :这是我理想的界面:
abstract class FileMonitor(root: Path) {
  def start(): Unit
  def onCreate(path: Path): Unit
  def onModify(path: Path): Unit
  def onDelete(path: Path): Unit  
  def stop(): Unit
}

这样,我可以简单地将示例代码编写为:

val watcher = new FileMonitor(myFile) {
  override def onCreate(path: Path) = println(s"$path got created")  
  override def onModify(path: Path) = println(s"$path got modified")    
  override def onDelete(path: Path) = println(s"$path got deleted")  
}
watcher.start()

好的,让我们尝试使用Java Thread修改第一个示例,以便我们可以公开“我理想的接口”:

trait FileMonitor {                               // My ideal interface
  val root: Path                                  // starting file  
  def start(): Unit                               // start the monitor 
  def onCreate(path: Path) = {}                   // on-create callback 
  def onModify(path: Path) = {}                   // on-modify callback 
  def onDelete(path: Path) = {}                   // on-delete callback 
  def onUnknownEvent(event: WatchEvent[_]) = {}   // handle lost/discarded events
  def onException(e: Throwable) = {}              // handle errors e.g. a read error
  def stop(): Unit                                // stop the monitor
}

这是一个非常基本的基于线程的实现:

class ThreadFileMonitor(val root: Path) extends Thread with FileMonitor {
  setDaemon(true)        // daemonize this thread
  setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler {
    override def uncaughtException(thread: Thread, exception: Throwable) = onException(exception)    
  })

  val service = root.getFileSystem.newWatchService()

  override def run() = Iterator.continually(service.take()).foreach(process)

  override def interrupt() = {
    service.close()
    super.interrupt()
  }

  override def start() = {
    watch(root)
    super.start()
  }

  protected[this] def watch(file: Path): Unit = {
    file.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)
  }

  protected[this] def process(key: WatchKey) = {
    key.pollEvents() foreach {
      case event: WatchEvent[Path] => dispatch(event.kind(), event.context())      
      case event => onUnknownEvent(event)
    }
    key.reset()
  }

  def dispatch(eventType: WatchEvent.Kind[Path], file: Path): Unit = {
    eventType match {
      case ENTRY_CREATE => onCreate(file)
      case ENTRY_MODIFY => onModify(file)
      case ENTRY_DELETE => onDelete(file)
    }
  }
}

上面看起来更干净! 现在,我们可以通过简单地实现onCreate(path)onModify(path)onDelete(path)等来观看文件,而不用onModify(path) JavaDocs的细节。

  • 异常处理 :上面已经完成了。 每当我们遇到异常时,就会调用onException ,并且调用者可以通过实现它来决定下一步该做什么。
  • 递归监视 :Java API 不允许递归监视目录 。 我们需要修改watch(file)以递归地附加观察者:
def watch(file: Path, recursive: Boolean = true): Unit = {
  if (Files.isDirectory(file)) {
    file.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)              
     // recursively call watch on children of this file  
     if (recursive) { 
       Files.list(file).iterator() foreach {f => watch(f, recursive)}
     } 
  }
}
  • 监视 常规文件 :如前所述,Java API 仅可以监视目录 。 我们可以监视单个文件的一种方法是在其父目录上设置监视程序,并且仅当事件在文件本身上触发时才做出反应。
override def start() = {
  if (Files.isDirectory(root)) {
    watch(root, recursive = true) 
  } else {
    watch(root.getParent, recursive = false)
  }
  super.start()
}

并且,现在在process(key) ,我们确保仅对目录或该文件做出反应:

def reactTo(target: Path) = Files.isDirectory(root) || (root == target)

而且,我们现在在dispatch前检查:

case event: WatchEvent[Path] =>
  val target = event.context()
  if (reactTo(target)) {
    dispatch(event.kind(), target)
  }
  • 自动监视新项目 :Java API 不会自动监视任何新的子文件 。 我们可以通过在触发ENTRY_CREATE事件时将观察者自己附加到process(key)来解决此问题:
if (reactTo(target)) {
  if (Files.isDirectory(root) && event.kind() == ENTRY_CREATE) {
    watch(root.resolve(target))
  }
  dispatch(event.kind(), target)
}

放在一起,我们有了最终的FileMonitor.scala

class ThreadFileMonitor(val root: Path) extends Thread with FileMonitor {
  setDaemon(true) // daemonize this thread
  setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler {
    override def uncaughtException(thread: Thread, exception: Throwable) = onException(exception)    
  })

  val service = root.getFileSystem.newWatchService()

  override def run() = Iterator.continually(service.take()).foreach(process)

  override def interrupt() = {
    service.close()
    super.interrupt()
  }

  override def start() = {
    if (Files.isDirectory(root)) {
      watch(root, recursive = true) 
    } else {
      watch(root.getParent, recursive = false)
    }
    super.start()
  }

  protected[this] def watch(file: Path, recursive: Boolean = true): Unit = {
    if (Files.isDirectory(file)) {
      file.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)
      if (recursive) {
        Files.list(file).iterator() foreach {f => watch(f, recursive)}
      }  
    }
  }

  private[this] def reactTo(target: Path) = Files.isDirectory(root) || (root == target)

  protected[this] def process(key: WatchKey) = {
    key.pollEvents() foreach {
      case event: WatchEvent[Path] =>
        val target = event.context()
        if (reactTo(target)) {
          if (Files.isDirectory(root) && event.kind() == ENTRY_CREATE) {
            watch(root.resolve(target))
          }
          dispatch(event.kind(), target)
        }
      case event => onUnknownEvent(event)
    }
    key.reset()
  }

  def dispatch(eventType: WatchEvent.Kind[Path], file: Path): Unit = {
    eventType match {
      case ENTRY_CREATE => onCreate(file)
      case ENTRY_MODIFY => onModify(file)
      case ENTRY_DELETE => onDelete(file)
    }
  }
}

现在,我们已经解决了所有难题,并摆脱了WatchService API的复杂性,我们仍然与基于线程的API紧密相连。 我们将使用上述类来公开一个不同的并发模型,即actor模型,而不是使用Akka设计一个反应性,动态和灵活的文件系统监视程序。 尽管Akka actor构造不在本文讨论范围之内,但是我们将展示一个使用ThreadFileMonitor的非常简单的ThreadFileMonitor

import java.nio.file.{Path, WatchEvent}

import akka.actor._

class FileWatcher(file: Path) extends ThreadFileMonitor(file) with Actor {
  import FileWatcher._

  // MultiMap from Events to registered callbacks
  protected[this] val callbacks = newMultiMap[Event, Callback]  

  // Override the dispatcher from ThreadFileMonitor to inform the actor of a new event
  override def dispatch(event: Event, file: Path) = self ! Message.NewEvent(event, file)  

  // Override the onException from the ThreadFileMonitor
  override def onException(exception: Throwable) = self ! Status.Failure(exception)

  // when actor starts, start the ThreadFileMonitor
  override def preStart() = super.start()   
  
  // before actor stops, stop the ThreadFileMonitor
  override def postStop() = super.interrupt()

  override def receive = {
    case Message.NewEvent(event, target) if callbacks contains event => 
       callbacks(event) foreach {f => f(event -> target)}

    case Message.RegisterCallback(events, callback) => 
       events foreach {event => callbacks.addBinding(event, callback)}

    case Message.RemoveCallback(event, callback) => 
       callbacks.removeBinding(event, callback)
  }
}

object FileWatcher {
  type Event = WatchEvent.Kind[Path]
  type Callback = PartialFunction[(Event, Path), Unit]

  sealed trait Message
  object Message {
    case class NewEvent(event: Event, file: Path) extends Message
    case class RegisterCallback(events: Seq[Event], callback: Callback) extends Message
    case class RemoveCallback(event: Event, callback: Callback) extends Message
  }
}

这使我们能够动态注册和删除对文件系统事件做出响应的回调:

// initialize the actor instance
val system = ActorSystem("mySystem") 
val watcher: ActorRef = system.actorOf(Props(new FileWatcher(Paths.get("/home/pathikrit"))))

// util to create a RegisterCallback message for the actor
def when(events: Event*)(callback: Callback): Message = {
  Message.RegisterCallback(events.distinct, callback)
}

// send the register callback message for create/modify events
watcher ! when(events = ENTRY_CREATE, ENTRY_MODIFY) {   
  case (ENTRY_CREATE, file) => println(s"$file got created")
  case (ENTRY_MODIFY, file) => println(s"$file got modified")
}

翻译自: https://www.javacodegeeks.com/2015/12/reactive-file-system-monitoring-using-akka-actors.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值