Java ClassLoader实现热加载
关于热加载
我们这里主要使用ClassLoader来实现,ClassLoader具有一个明显的缺陷——无法卸载旧资源,但是对于小缝小补还是便捷和易于维护的。
定义ClassHotLoader
package cn.itest.loader.mock; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class ClassHotLoader { public static ClassHotLoader instance = null; private CustomClassLoader classLoader; private String classPath; private ClassHotLoader(String classPath) { this.classPath = classPath; } public static ClassHotLoader get(String classPath) { if (instance == null) { synchronized (ClassHotLoader.class) { if (instance == null) { instance = new ClassHotLoader(classPath); } } } return instance; } /** * 自定义类加载引擎 * * @param name * @return * @throws ClassNotFoundException */ public Class<?> loadClass(String name) throws ClassNotFoundException { synchronized (this) { classLoader = new CustomClassLoader(this.classPath); Class<?> findClass = classLoader.findClass(name); if (findClass != null) { return findClass; } } return classLoader.loadClass(name); } public static class CustomClassLoader extends ClassLoader { private String classPath = null; public CustomClassLoader(String classPath) { super(ClassLoader.getSystemClassLoader()); this.classPath = classPath; } /** * 重写findClass */ @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] classByte = null; classByte = readClassFile(name); if (classByte == null || classByte.length == 0) { throw new ClassNotFoundException("ClassNotFound : " + name); } return this.defineClass(name, classByte, 0, classByte.length); } /** * 读取类文件 * * @param name * @return * @throws ClassNotFoundException */ private byte[] readClassFile(String name) throws ClassNotFoundException { String fileName = name.replace(".", "/") + ".class"; File classFile = new File(this.classPath, fileName); if (!classFile.exists() || classFile.isDirectory()) { throw new ClassNotFoundException("ClassNotFound : " + name); } FileInputStream fis = null; try { fis = new FileInputStream(classFile); int available = fis.available(); int bufferSize = Math.max(Math.min(1024, available), 256); ByteBuffer buf = ByteBuffer.allocate(bufferSize); byte[] bytes = null; FileChannel channel = fis.getChannel(); while (channel.read(buf) > 0) { buf.flip(); bytes = traslateArray(bytes, buf); buf.clear(); } return bytes; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { closeIOQuiet(fis); } return null; } /** * 数组转换 * * @param bytes * @param _array * @return */ public byte[] traslateArray(byte[] bytes, ByteBuffer buf) { if (bytes == null) { bytes = new byte[0]; } byte[] _array = null; if (buf.hasArray()) { _array = new byte[buf.limit()]; System.arraycopy(buf.array(), 0, _array, 0, _array.length); } else { _array = new byte[0]; } byte[] _implyArray = new byte[bytes.length + _array.length]; System.arraycopy(bytes, 0, _implyArray, 0, bytes.length); System.arraycopy(_array, 0, _implyArray, bytes.length, _array.length); bytes = _implyArray; return bytes; } /** * 关闭io流 * * @param closeable */ public static void closeIOQuiet(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
通过上述程序,我们指定了CLASSPATH的位置,因此,对于类更新,我们需要一个专门监听类文件改变的工具。
定义文件观察者
package cn.itest.loader.mock; import java.io.File; import java.util.Observable; import java.util.concurrent.TimeUnit; public class ClassFileObserver extends Observable { private ObserveTask observeTask; public ClassFileObserver(String path) { observeTask = new ObserveTask(path, this); } /** * 用于更新观察者 * * @param objects */ public void sendChanged(Object[] objects) { super.setChanged();// 必须调用,否则通知无效 super.notifyObservers(objects); } public void reset(String path) { if (observeTask != null && !observeTask.isStop) { observeTask.isStop = false; observeTask.interrupt(); observeTask = null; } observeTask = new ObserveTask(path, this); } /** * 开始观察文件 */ public void startObserve() { if (isStop()) { System.out.println("--启动类文件更新监控程序--"); observeTask.isStop = false; observeTask.start(); } } public boolean isStop() { return observeTask != null && !observeTask.isStop; } /** * 停止观察文件 */ public void stopObserve() { System.out.println("--停止类文件更新监控程序--"); observeTask.isStop = true; } public static class ObserveTask extends Thread { private String path; private long lastLoadTime; private boolean isStop = false; private ClassFileObserver observable; public ObserveTask(String path, ClassFileObserver obs) { this.path = path; this.observable = obs; this.lastLoadTime = -1; } public void run() { while (!isStop && this.isAlive()) { synchronized (this) { long loadTime = getLastLoadTime(); if (loadTime != this.lastLoadTime) { observable.sendChanged(new Object[] { loadTime, this.lastLoadTime }); this.lastLoadTime = loadTime; } try { TimeUnit.SECONDS.sleep(3); // 每隔3秒检查一次文件 } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 将文件最后修改时间作为最后加载时间 * * @return */ public long getLastLoadTime() { if (path == null) { return -1; } File f = new File(path); if (!f.exists() || f.isDirectory()) { // 不需要监控目录 return -1; } return f.lastModified(); } } }
测试示例
测试类:
package cn.itest; public class Person { public void sayHello(){ System.out.println("hello world! 我是李四!"); } }
注意:将此类文件连同类目录拷贝到CLASSPATH下
测试说明:网上很多例子将CLASSPATH设置为【项目路径/bin/classes】,这种方式有一个弊端,那就是当前项目的此路径本身就是CLASSPATH之一,因此,我们可以按照自己的指定目录来设置。
package cn.itest.loader.mock; import java.io.File; import java.lang.reflect.Method; import java.util.Observable; import java.util.Observer; public class ClassLoaderTest { public static void main(String[] args) { final String classPath = "E:/share/"; final String className = "cn.itest.Person"; final String fileName = className.replace(".", "/") + ".class"; File f = new File(classPath, fileName); ClassFileObserver cfo = new ClassFileObserver(f.getAbsolutePath()); cfo.addObserver(new Observer() { public void update(Observable o, Object arg) { try { Object[] loadTimes = (Object[]) arg; System.out.println(loadTimes[0] + " <---> " + loadTimes[1]);// 新旧时间对比 Class<?> loadClass = ClassHotLoader.get(classPath) .loadClass(className); Object person = loadClass.newInstance(); Method sayHelloMethod = loadClass.getMethod("sayHello"); sayHelloMethod.invoke(person); } catch (Exception e) { e.printStackTrace(); } } }); cfo.startObserve(); } }
测试结果:
--启动类文件更新监控程序-- 1514693003306 <---> -1 hello world! 我是我是张三! 1514693054791 <---> 1514693003306 hello world! 我是李四!
特别事项
①测试类中在加载cn.itest.Person的时候,使用的是CustomClassLoader的findClass方法。 而不是loadClass方法, 因为loadClass方法由于双亲委派模式,会将cn.itest.Person交给CustomClassLoader的父ClassLoader进行加载。 而其父ClassLoader对加载的Class做了缓存,如果发现该类已经加载过, 就不会再加载第二次。 就算改类已经被改变
②同一个ClassLoader不能多次加载同一个类。 如果重复的加载同一个类 , 将会抛出 loader (instance of cn/itest/loader/mock/CustomClassLoader): attempted duplicate class definition for name: “cn/itest/Person” 异常。 所以,在替换Class的时候, 加载该Class的ClassLoader也必须用新的。
③如果想要使用loadClass方法加载类,那么需要重写的方法除了loadClass,必须还得重写findLoadedClass
④springloaded实现热加载
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <!-- spring热部署--> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.6.RELEASE</version> </dependency> </dependencies> </plugin> </plugins> </build>
⑤spring-boot-devtools实现热部署
注意:代码添加到项目依赖中,而不是构建依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
原文地址:https://my.oschina.net/ososchina/blog/1599977
相关推荐
-
JVM虚拟机栈——JAVA方法的消亡史 Java基础
2020-5-28
-
这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程? Java基础
2020-6-15
-
Synchronize 关键字理 Java基础
2020-5-30
-
资深架构师带你从JVM层面了解线程的启动和停止 Java基础
2019-8-21
-
Java & PhantomJs 实现html输出图片 Java基础
2020-5-30
-
使用Netty,我们到底在开发些什么? Java基础
2019-3-15
-
让机器读懂视频:亿级淘宝视频背后的多模态AI算法揭秘 Java基础
2020-7-2
-
文件下载时前后台MD5校验 Java基础
2018-12-11
-
如何通过努力出书,如何写有畅销资质的书,本文汇集了多位计算机图书作者的经验 Java基础
2019-6-3
-
深入浅出synchronized Java基础
2019-1-24