|
关于热加载
热修复当前是很流行的技术,在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;
System.arraycopy(buf.array(), 0, _array, 0, _array.length);
} else {
_array = new byte[0];
}
byte[] _implyArray = new byte;
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
原文出处:https://my.oschina.net/ososchina/blog/1599977
程序猿的技术大观园:www.javathinker.net
|
|