当前位置: 首页 > 工具软件 > JReloader > 使用案例 >

基于NIO2.0的WatchService实现JReloader,不重启jvm 的debug

越欣怡
2023-12-01

       众所周知,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,理论上来说应该比用线程轮询的方式好点,不过仅是猜测,具体我也没测试过,如果哥们你想测试一下,一定要告诉我。

    最后,文笔比较差,如果各位看官有什么看不懂的地方,可以直接私信我。

 

 

 类似资料: