Java ClassLoader实现热加载


声明:本文转载自https://my.oschina.net/ososchina/blog/1599977,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

关于热加载

热修复当前是很流行的技术,在Android平台,我们可以使用Andfix、Hotfix和Tinker等技术。实际上,在java程序中,热修复技术远比Android多的多。最原始的ClassLoader重新加载,还有最时髦的javassist或者asm工具包,甚至我们可以借助JNI、J2V8或者RPC(WebService,JSONRPC,dwr,Thrift)方式来实现功能的修复和替换。

我们这里主要使用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

 

本文发表于2018年01月01日 08:38
(c)注:本文转载自https://my.oschina.net/ososchina/blog/1599977,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1833 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1