java 热加载class_Java ClassLoader实现热加载

本文介绍了如何使用Java的ClassLoader实现类的热加载,通过自定义ClassHotLoader和ClassFileObserver来监控并加载修改后的类文件,避免了双亲委派模式的影响,并提供了测试示例。此外,还提到了SpringLoaded和Spring Boot DevTools实现热部署的方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于热加载

热修复当前是很流行的技术,在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

④springloaded实现热加载

org.springframework.boot

spring-boot-maven-plugin

org.springframework

springloaded

1.2.6.RELEASE

⑤spring-boot-devtools实现热部署

注意:代码添加到项目依赖中,而不是构建依赖

org.springframework.boot

spring-boot-devtools

true

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值