在运行时重新加载Groovy脚本

时间:2019-05-06 13:45:45

标签: java groovy runtime

我希望能够从我的Java应用程序执行groovy脚本。 如果需要,我想在运行时重新加载groovy脚本。根据他们的tutorials,我可以做这样的事情:

long now = System.currentTimeMillis();
for(int i = 0; i < 100000; i++) {
    try {
        GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine("");                
        System.out.println(groovyScriptEngine.run("myScript.groovy", new Binding()););
    } catch (Exception e) {
        e.printStackTrace();
    }
}
long end = System.currentTimeMillis();

System.out.println("time " + (end - now));//24 secs

myScript.groovy

"Hello-World"

这很好,每次我在myScript.groovy中更改一行时,脚本都会重新加载。

问题在于这不是省时的,它每次都是从文件中解析脚本。

还有其他选择吗?例如,一些更聪明的工具可以检查脚本是否已被解析以及自上次解析以来是否未更改,因此无需再次解析。

1 个答案:

答案 0 :(得分:1)

<<根据评论进行了编辑>>

就像其中一条注释中提到的那样,如果需要性能,则必须将解析(缓慢)与执行(快速)分开。

对于脚本源的响应式重新加载,我们可以例如使用java nio watch service

import groovy.lang.*
import java.nio.file.*

def source = new File('script.groovy')
def shell  = new GroovyShell()
def script = shell.parse(source.text)

def watchService = FileSystems.default.newWatchService()
source.canonicalFile.parentFile.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY)

boolean keepWatching = true 
Thread.start { 
  while (keepWatching) {
    def key = watchService.take()
    if (key.pollEvents()?.any { it.context() == source.toPath() }) {
      script = shell.parse(source.text)
      println "source reloaded..."
    }
    key.reset()
  }
}

def binding = new Binding()
def start = System.currentTimeMillis()
for (i=0; i<100; i++) {
  script.setBinding(binding)
  def result = script.run()
  println "script ran: $result"
  Thread.sleep(500)
} 
def delta = System.currentTimeMillis() - start
println "took ${delta}ms"

keepWatching = false

上面的代码启动了一个单独的监视程序线程,该线程使用java监视服务监视父目录中的文件修改,并在检测到修改时重新加载脚本源。假定使用Java版本7或更高版本。睡眠就在那儿,以便更轻松地处理代码;如果要衡量性能,则应该自然删除。

将字符串System.currentTimeMillis()存储在script.groovy中并运行上面的代码将使其每秒循环两次。在循环期间对script.groovy进行修改会导致:

~> groovy solution.groovy 
script ran: 1557302307784
script ran: 1557302308316
script ran: 1557302308816
script ran: 1557302309317
script ran: 1557302309817
source reloaded...
script ran: 1557302310318
script ran: 1557302310819
script ran: 1557302310819
source reloaded...

只要对源文件进行了更改,就会在其中打印source reloaded...行。

我不确定Windows,但是至少在linux上,我相信Java在后台使用fsnotify系统,这应该使文件监视部分发挥作用。

应该指出,如果我们真的很倒霉,脚本变量将由观察者线程在两行之间重置:

  script.setBinding(binding)
  def result = script.run()

会破坏代码,因为重新加载的脚本实例将没有绑定集。为了解决这个问题,我们可以使用例如锁:

import groovy.lang.*
import java.nio.file.*
import java.util.concurrent.locks.ReentrantLock

def source = new File('script.groovy')
def shell  = new GroovyShell()
def script = shell.parse(source.text)

def watchService = FileSystems.default.newWatchService()
source.canonicalFile.parentFile.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY)

lock = new ReentrantLock()

boolean keepWatching = true 
Thread.start { 
  while (keepWatching) {
    def key = watchService.take()
    if (key.pollEvents()?.any { it.context() == source.toPath() }) {
      withLock { 
        script = shell.parse(source.text)
      }
      println "source reloaded..."
    }
    key.reset()
  }
}

def binding = new Binding()
def start = System.currentTimeMillis()
for (i=0; i<100; i++) {
  withLock { 
    script.setBinding(binding)
    def result = script.run()
    println "script ran: $result"
  }
  Thread.sleep(500)
} 
def delta = System.currentTimeMillis() - start
println "took ${delta}ms"

keepWatching = false

def withLock(Closure c) {
  def result
  lock.lock()
  try { 
    result = c()
  } finally { 
    lock.unlock()
  }
  result
}

这会使代码有些混乱,但可以使我们免受并发问题的困扰,这些问题往往很难追踪。