>>分享Java编程技术,对《Java面向对象编程》等书籍提供技术支持 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 24366 个阅读者 刷新本主题
 * 贴子主题:  Java ClassLoader实现热加载 回复文章 点赞(0)  收藏  
作者:sunshine    发表时间:2018-01-01 11:07:25     消息  查看  搜索  好友  邮件  复制  引用

关于热加载

热修复当前是很流行的技术,在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
网站系统异常


系统异常信息
Request URL: http://www.javathinker.net/WEB-INF/lybbs/jsp/topic.jsp?postID=354&pages=31

java.lang.NullPointerException

如果你不知道错误发生的原因,请把上面完整的信息提交给本站管理人员