最近几天看了一些热加载方面的资料,然后动手搭建了个基本框架,暂时实现了有限制性的热加载功能,先和大家分享一下
整个框架结构如下图所示:
功能的实现上,主要分为2步:
1. 编译.java文件,生成.class文件。
这里我使用了eclipse的编译器类 EclipseCompiler 来实现
2.加载编译后的.class文件。
自定义一个类加载器,每次热更新都新建一个类加载器实例来实现。java传统的类加载是走双亲委派模式,当要加载一个类的时候,需要先由自身的父加载器来加载,只有父加载器还未加载过该类,才能由自身来加载。(我们也可以覆盖ClassLoader.loadClass()方法,打破这个规则)。由于不同的类加载器实例具有独立的空间,而且处于同一继承等级,具有同一个父加载器,这就确保了实例A加载的类,实例B也能重新加载,互不影响。
好了,下面就看代码实现吧。
package hotswap.test;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import hotswap.HotswapEngine;
public class Test {
/**
* @author Nate
* @date 2013-11-19
*/
public static void main(String[] args) throws Exception {
//需要热加载的类路径
final String classPath = System.getProperty("user.dir") + "/hotcodes";
//需要热加载执行的类源文件路径
final String javaFilePath = classPath + "/codes/A.java";
HotswapEngine engine = new HotswapEngine();
//第一次加载执行
Class<?> clazz1 = engine.reload(classPath, javaFilePath);
evalMainMethod(clazz1);
System.err.println("==> 请修改文件,然后按任意键重新加载");
System.in.read();
//修改代代码后,再次加载执行
Class<?> clazz2 = engine.reload(classPath, javaFilePath);
evalMainMethod(clazz2);
}
/**
* 执行指定类的main方法
*
* @author Nate
* @date 2013-11-19
*/
public static void evalMainMethod(Class<?> clazz, String...args) {
try {
Method mainMethod = clazz.getMethod("main",
new Class[] { String[].class });
if (mainMethod == null) return;
int modifiers = mainMethod.getModifiers();
if (!Modifier.isPublic(modifiers) ||
!Modifier.isStatic(modifiers))
return;
mainMethod.invoke(null, new Object[]{args});
} catch (Exception e) {
e.printStackTrace();
}
}
}
package hotswap;
import hotswap.JavaFileManager.JavaSourceObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaCompiler.CompilationTask;
import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
public class HotswapEngine {
private final JavaCompiler compiler = new EclipseCompiler();
public Class<?> reload(String classPath, String javaFilePath) {
try {
File javaFile = new File(javaFilePath);
if (!javaFile.exists() || !javaFile.isFile())
return null;
//文件名
String fileName = javaFile.getName();
//完全类名
String classFullName = javaFilePath.substring(classPath.length() + 1, javaFilePath.lastIndexOf('.'));
classFullName = classFullName.replace('/', '.').replace('\\', '.');
//编译选项
List<String> options = new ArrayList<String>();
options.add("-warn:-enumSwitch");
options.add("-g");
options.add("-deprecation");
options.add("-1.7");
options.add("-encoding");
options.add("UTF-8");
//options.add("-sourcepath");
//options.add(classPath);
options.add("-classpath");
options.add(classPath);
//java文件管理器
JavaFileManager fileManager = new JavaFileManager();
//编译单元集合
List<JavaFileObject> compilationUnits = new ArrayList<JavaFileObject>(1);
//加载java文件源码
String sourceCode = loadJavaSourceCode(javaFile);
//生成java源码对象
JavaSourceObject sourceObj = JavaFileManager.newJavaSourceObject(fileName, sourceCode);
compilationUnits.add(sourceObj);
//执行编译任务
CompilationTask task = compiler.getTask(null, fileManager, null, options, null, compilationUnits);
if (!task.call()) {
//编译失败
return null;
}
//获取编译后的class文件
Map<String, byte[]> classBytes = fileManager.getClassBytes();
fileManager.close();
//加载class文件
HotswapClassLoader loader = new HotswapClassLoader(classBytes, classPath, ClassLoader.getSystemClassLoader());
Class<?> clazz = loader.findClass(classFullName);
return clazz;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String loadJavaSourceCode(File javaFile) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(javaFile)));
char[] arr = new char[8 * 1024];
StringBuilder buf = new StringBuilder();
int numChars;
while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
buf.append(arr, 0, numChars);
}
return buf.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
package hotswap;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class HotswapClassLoader extends URLClassLoader {
private final Map<String, byte[]> classBytes;
public HotswapClassLoader(Map<String, byte[]> classBytes, String classPath, ClassLoader parent) {
super(toURLs(classPath), parent);
this.classBytes = classBytes;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
//先从我们编译后的class文件缓存里面找
byte buf[] = classBytes.get(className);
if (buf != null) {
classBytes.put(className, null);
return defineClass(className, buf, 0, buf.length);
}
return super.findClass(className);
}
private static URL[] toURLs(String classPath) {
if (classPath == null) {
return new URL[0];
}
List<URL> list = new ArrayList<URL>();
StringTokenizer st = new StringTokenizer(classPath, File.pathSeparator);
while (st.hasMoreTokens()) {
String token = st.nextToken();
File file = new File(token);
if (file.exists()) {
try {
list.add(file.toURI().toURL());
} catch (MalformedURLException mue) {}
} else {
try {
list.add(new URL(token));
} catch (MalformedURLException mue) {}
}
}
URL res[] = new URL[list.size()];
list.toArray(res);
return res;
}
}
package hotswap;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.CharBuffer;
import java.util.HashMap;
import java.util.Map;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.JavaFileObject.Kind;
import org.eclipse.jdt.internal.compiler.tool.EclipseFileManager;
public class JavaFileManager extends EclipseFileManager {
//保存已经重新编译的class文件
protected Map<String, byte[]> classBytes;
public JavaFileManager() {
super(null, null);
classBytes = new HashMap<String, byte[]>();
}
public Map<String, byte[]> getClassBytes() {
return classBytes;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
if (kind == Kind.CLASS) {
return new JavaClassObject(className.replace('/', '.'));
}
return super.getJavaFileForOutput(location, className, kind, sibling);
}
/**
* 新建一个java源文件对象
*
* @author Nate
* @param name 文件名
* @param source java源码字符串
*
* @date 2013-11-21
*/
public static JavaSourceObject newJavaSourceObject(String name, String source) {
return new JavaSourceObject(name, source);
}
/**
* java class文件对象(.class)
*
* @author Nate
* @date 2013-11-21
*/
public class JavaClassObject extends SimpleJavaFileObject {
protected final String name;
protected JavaClassObject(String name) {
super(toURI(name), Kind.CLASS);
this.name = name;
}
/**
* 编译器编译时回调, 这里我们用自己的输出流来接受编译后的class文件
*/
@Override
public OutputStream openOutputStream() throws IOException {
return new FilterOutputStream(new ByteArrayOutputStream()) {
@Override
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream)out;
classBytes.put(name, bos.toByteArray());
}
};
}
}
/**
* java 源文件对象(.java)
*
* @author Nate
* @date 2013-11-21
*/
public static class JavaSourceObject extends SimpleJavaFileObject {
final String source;
JavaSourceObject(String name, String source) {
super(toURI(name), Kind.SOURCE);
this.source = source;
}
@Override
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(source);
}
}
static final String JAVA_FILE_EXT = ".java";
static URI toURI(String name) {
File file = new File(name);
if (file.exists()) {
return file.toURI();
}
try {
final StringBuilder newUri = new StringBuilder();
newUri.append("file:///");
newUri.append(name.replace('.', '/'));
if (name.endsWith(JAVA_FILE_EXT)) {
newUri.replace(newUri.length() - JAVA_FILE_EXT.length(), newUri.length(), JAVA_FILE_EXT);
}
return URI.create(newUri.toString());
} catch (Exception exp) {
return null;
}
}
}
package codes;
public class A {
public static void main(String args[]) {
System.out.println("aaaaaaaaaaaaaaaaa");
//System.out.println("AAAAAAAAAAAAAAAAA");
//看看引用的类能不能也热更新
new B().say();
}
}
package codes;
public class B {
public void say() {
System.out.println("bbbbbbbbbbbbbbbbb");
//System.out.println("BBBBBBBBBBBBBBBBB");
}
}
运行测试类的结果:
这里有个地方要注意一下的,需要将主代码和需要热加载的代码分开来放,并且要把主代码运行的类路径设为只包含主代码的文件夹,不能让主代码的类路径覆盖了主代码和需要热加载的代码,不然的话,新的加载器实例会加载不了,因为都被父加载器先加载了。 另外还可以更直接点,把需要热加载的代码放到另外一个项目。