Java ClassLoader实现热加载

Java基础

浏览数:281

2020-7-3


关于热加载

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

 

 

作者:IamOkay