Pure JS (2): 热部署 (利用 JDK 7 NIO 监控文件变化)
接着上一篇文章( http://xxing22657-yahoo-com-cn.iteye.com/blog/1052485)的话题,我们来谈谈服务器端 JS 的热部署问题。
由于 JavaScript 是动态语言,动态编译并执行脚本并不困难。所以关键是监控文件变化。
这里推荐使用 JDK 7 NIO 中新增的 Watch Service API。Watch Service API 将尽可能使用操作系统的文件 API ,当操作系统不支持时,则使用轮询。简而言之,就是 JNI + 轮询,自己实现轮询倒不难,但要实现跨平台的 JNI 调用就很啰嗦了。
JDK 7 还没有正式发布,可以先到这里下一个 Early Access 版 :
http://jdk7.java.net/
安装完后可以通过 eclipse 的 Window -> Preference -> Java -> Intalled JREs 添加刚安装的 JRE,并勾选为默认的 JRE 环境。
关于 Watch Service API 的使用说明可以参考这里:
http://download.oracle.com/javase/tutorial/essential/io/notification.html
前期准备
首先,我们在 src 目录下创建 purejs package ,将之前编写的 JSServer,JSEngine 和 JSServlet 都移动到这个 package 下。因为后面将会用到 import static 的写法,没有 package 名称的话会报错。
然后,我们需要在 JSEngine 类中增加执行单个文件的方法( excuteFile(File file) ),并在 excuteFiles(String dir) 方法中引用它。
现在的 JSEngine 应该是这样的:
public class JSEngine { // Declaration of engine, compliable and invocable ... static { // Initialize of engine, compliable and invocable ... } public static void excuteFiles(String dir) throws ScriptException, IOException { for (File file : new File(dir).listFiles()) { excuteFile(file); } } public static void excuteFile(File file) throws ScriptException, IOException { FileReader reader = new FileReader(file); compliable.compile(reader).eval(); reader.close(); } // Defination of invoke ... }
增加文件监控类 FileChangeMonitor
实现思路是:
1. 创建一个新的线程。
2. 在线程中创建 Watch Service 的实例 watcher 。
3. 在 watcher 上注册文件的创建和修改事件。
4. 在循环中使用 watcher.take() 等待事件的发生。
5. 遍历捕捉到的所有事件,获取文件名,通知 Lisener 文件的更改。
具体实现如下:
package purejs; import static java.nio.file.StandardWatchEventKinds.*; import java.io.*; import java.nio.file.*; public class FileChangeMonitor { public interface FileChangeListener { public void fileChanged(File file); } public static void monit(final String dir, final FileChangeListener listener) { new Thread() { public void run() { Path path = new File(dir).toPath(); WatchService watcher; try { watcher = FileSystems.getDefault().newWatchService(); path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY); } catch (IOException e) { e.printStackTrace(); return; } while (true) { WatchKey key; try { key = watcher.take(); } catch (InterruptedException _) { return; } processKey(path, key, listener); } }; }.start(); } private static void processKey(Path path, WatchKey key, FileChangeListener listener) { for (WatchEvent> event : key.pollEvents()) { if (event.kind() == OVERFLOW) { continue; } Path fileName = (Path) event.context(); File file = path.resolve(fileName).toFile(); listener.fileChanged(file); } key.reset(); } }
在 JSServer 中引入 FileChangeMonitor
将 main 函数的执行分成两个步骤:
1. 加载脚本。这里又分为两个步骤:
【执行脚本】 直接调用 JSEngine 执行“ scripts ”目录下的脚本。
【设置监控】 调用 FileChangeMonitor.monit(...),文件变更时再次执行脚本。
2. 启动 Server,与之前相同。
public class JSServer { public static void main(String[] args) throws Exception { loadScripts(); startServer(); } private static void loadScripts() throws ScriptException, IOException { JSEngine.excuteFiles("scripts"); FileChangeMonitor.monit("scripts", new FileChangeListener() { public void fileChanged(File file) { try { JSEngine.excuteFile(file); } catch (ScriptException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }); } // Defination of startServer() and configContext(...) }
运行并测试
运行程序,输入 http://localhost:8080,可以看到浏览器输出:
Hello,Pei Xiaoxing!
修改 api.js ,比如改为:
api = { sayHello: function(params) { return 'Hello, again!'; } };
保存修改,再次访问 http://localhost:8080,可以看到输出变化了:
Hello, again!
删除文件?
我们没有对文件删除的行为进行检查,但仍可以“曲线救国”。
比如我们不想要“ api ”这个对象了,可以把包含如下内容的文件放入 “ scripts ” :
if (typeof api !== 'undefined') { delete api; }
因为我们监控了文件的创建,所以这个脚本将很快被执行。这样一来,“ api ”对象就被删除了。
再次访问 http://localhost:8080 ,控制台将输出如下错误:
ReferenceError: "api" is not defined.
然后就可以把这个文件和 api.js 一起删除了。
最新版的 Java 7 中,java.nio.file.StandardWatchEventKind 已改为 java.nio.file.StandardWatchEventKinds,文章已更新。
下载附件后,请将 FileChangeMonitor.java 中的 StandardWatchEventKind 改为 StandardWatchEventKinds