众所周知,java编程中,更改了class文件的话需要重启JVM,从而导致Java的开发效率问题一直遭人诟病,不过,也可以用另外一种方法来避免这个问题,这个就是国外的大神写的JReloader,网址:http://code.google.com/p/jreloader/,用这个可以实时的修改class文件,而避免重启JVM,提高开发效率。
其实Jreloader所用到的技术大家想必都知道,java agent 和ClassLoader,大致原理是利用map来保持class文件和ClassLoader的映射,如果class文件改变了,那么就用classLoader重新加载新生成的class文件。
实现细节请大家到网站上看源码,相关知识点呢,也请大家谷哥去吧。
到这里你估计要开骂了,说了这么一大堆废话,谁TM不知道啊,莫急,以下进入正题。
如上所述,java agent和classloader不是什么新名词了,大家都懂的,jreloader的关键是我如何知道一个class改变了呢?Jreloader的源码中是开启了一个线程来做的,如下代码所示:
private ReloadThread thread;
public Transformer() {
String[] dirNames = System.getProperty("jreloader.dirs", ".").split("\\,");
for (String dirName : dirNames) {
File d = new File(dirName).getAbsoluteFile();
log.info("Added class dir '" + d.getAbsolutePath() + "'");
scan(d, d);
}
findGroups();
log.info(" \\-- Found " + entries.size() + " classes");
thread = new ReloadThread();
thread.start();
}
注意到 thread.start();了吗?这个在类实例化的时候就启动了,下面我们再看看ReloadThread的真身。
class ReloadThread extends Thread {
public ReloadThread() {
super("ReloadThread");
setDaemon(true);
setPriority(MAX_PRIORITY);
}
@Override
public void run() {
try {
sleep(5000);
} catch (InterruptedException e) {
}
while (true) {
try {
sleep(3000);
} catch (InterruptedException e) {
}
if (System.getProperty("jreloader.pauseReload") != null) {
continue;
}
log.debug("Checking changes...");
List<Entry> aux = new ArrayList<Entry>(entries.values());
for (Entry e : aux) {
if (e.isDirty()) {
e.forceDirty();
}
}
for (Entry e : aux) {
if (e.isDirty() && e.parent == null) {
log.debug("Reloading " + e.name);
try {
reload(e);
} catch (Throwable t) {
log.error("Could not reload " + e.name, t);
System.err
.println("[JReloader:ERROR] Could not reload class "
+ e.name.replace('/', '.'));
}
e.clearDirty();
}
}
}
}
通过while(true),就知道这个线程会循环执行,然后检查每一个class文件isDirty也就是lastModified时间,如果这个时间变了,则需要重新用classloader加载。
看到这里,你或许说,jdk7或者以上的Nio2.0中不是有WatchService能实现监测文件是否改变的功能吗?
是的,本文就是要讨论这一点。
首先为了向下兼容,我姑且直接检查JDK的版本,如果是JDK7以上版本,就用新的实现,代码如下
if (ReloaderUtils.isEqualsOrAboveJDK7()) {
try {
Path path = Paths.get(dirNames[0]);
watchDir = new WatchDir(path, true);
watchServiceThread = new WatchServiceThread();
watchServiceThread.start();
} catch (IOException e) {
e.printStackTrace();
}
}else {
thread = new ReloadThread();
thread.start();
}
Path是新的NIO提供的一个类,类似于File,由于WatchService只能检测某一个具体文件夹下面的文件,如果文件夹下还有文件夹,它就爱莫能助了,所以我们必须要解决这个问题,这也就是WatchDir的由来,
WatchDir的代码如下:
private final WatchService watcher;
private final Map<WatchKey,Path> keys;
private final boolean recursive;
private boolean trace = false;
@SuppressWarnings("unchecked")
public static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
/**
* Register the given directory with the WatchService
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_MODIFY);
if (trace) {
Path prev = keys.get(key);
if (prev == null) {
System.out.format("register: %s\n", dir);
} else {
if (!dir.equals(prev)) {
System.out.format("update: %s -> %s\n", prev, dir);
}
}
}
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
public void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException
{
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Creates a WatchService and registers the given directory
*/
WatchDir(Path dir, boolean recursive) throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<WatchKey,Path>();
this.recursive = recursive;
if (recursive) {
System.out.format("Scanning %s ...\n", dir);
registerAll(dir);
System.out.println("Done.");
} else {
register(dir);
}
// enable trace after initial registration
this.trace = true;
}
/**
* Process all events for keys queued to the watcher
*/
void processEvents() {
for (;;) {
// wait for key to be signalled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
Path dir = keys.get(key);
if (dir == null) {
System.err.println("WatchKey not recognized!!");
continue;
}
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (kind == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
// print out event
System.out.format("%s: %s\n", event.kind().name(), child);
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (recursive && (kind == ENTRY_CREATE)) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(child);
}
} catch (IOException x) {
// ignore to keep sample readbale
}
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
break;
}
}
}
}
它通过扫描某个文件夹下面的所有子文件夹,把所有的文件夹加入到自己的监测管理中。
那WatchServiceThread又是干什么的呢,这是一个demon线程,下面是它的代码:
class WatchServiceThread extends Thread{
public WatchServiceThread() {
super("WatchServiceThread");
setDaemon(true);
setPriority(MAX_PRIORITY);
}
@Override
public void run() {
try {
processEvents();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
进入到主方法 processEvents();中
/**
* Process all events for keys queued to the watcher
* @throws InterruptedException
*/
private void processEvents() throws InterruptedException {
for (;;) {
Map<WatchKey,Path> keys = watchDir.getKeys();
WatchService wactherDir = watchDir.getWatcher();
// wait for key to be signalled
WatchKey key;
key = wactherDir.take();
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
break;
}
}
Path dir = keys.get(key);
if (dir == null) {
System.err.println("WatchKey not recognized!!");
continue;
}
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (kind == OVERFLOW) {
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = WatchDir.cast(event);
Path modifiedFile = ev.context();
// final Path modifiedFile = name.context();
Path classFilePath = modifiedFile.getFileName();
if (classFilePath.isAbsolute()) {
continue;
}
if (!classFilePath.toString().endsWith(".class")) {
continue;
}
String javaClassFileNameWithNoSuffix = ReloaderUtils.getFileRealPath(key, ev, dirNames);
// classFileName.substring(0, classFileName.length() - 6);
Entry entry = entries.get(javaClassFileNameWithNoSuffix);
if (entry.parent == null) {
try {
reload(entry);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
Path child = dir.resolve(modifiedFile);
// print out event
System.out.format("%s: %s\n", event.kind().name(), child);
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (watchDir.isRecursive() && (kind == ENTRY_CREATE)) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
watchDir.registerAll(child);
}
} catch (IOException x) {
// ignore to keep sample readbale
}
}
}
}
}
看到没有,这是一个无限循环,而且 key = wactherDir.take();会阻塞, 所以必须开一个线程,不能让它阻塞主线程。那以后的事情都交给NIO2.0的API了
当有class文件被修改后,线程会被唤醒,就会走 for (WatchEvent<?> event: key.pollEvents()) {}逻辑,这个时候如果直接用Path.getAbsolute()我们只能拿到相对路径,如果我们要拿到绝对路径,只能变通一下
public static String getFileRealPath(WatchKey key, WatchEvent<Path> watchEvent){
Path dir = (Path)key.watchable();
String fullPath = dir.resolve(watchEvent.context()).toString();
用这样能拿到Path 的绝对路径,拿到一个更改过的class的绝对路径以后,那事情该怎么办就怎么办了,
Entry entry = entries.get(javaClassFileNameWithNoSuffix);
if (entry.parent == null) {
try {
reload(entry);。。。。
通过文件名拿到map中额Entry, 然后reload这个更改过的class,问题搞定。
虽然是个小功能,但是也code + debug也花费了大半天的时间,可能是能力不行吧。。。。。。。。。最后总结一下,就是把原来用线程获得更改过的class文件的方式换成用基于JDK7上 WatchService的方式来实现,因为Watchservice是基于事件处理,又是原生的API,理论上来说应该比用线程轮询的方式好点,不过仅是猜测,具体我也没测试过,如果哥们你想测试一下,一定要告诉我。
最后,文笔比较差,如果各位看官有什么看不懂的地方,可以直接私信我。