什么是java热替换?
在运行的Java系统中进行类(对象)的替换升级。
实现热替换的
第一步就是动态编译,将.java文件编译成为.class文件
第二步是动态加载,编写一个自定义的类加载器,将编译好的类加载到运行环境中去
类的加载是由不同的加载器共同完成的
BootstrapClassLoader负责sum.boot.class.path路径下类的加载,默认为jre/lib或者-Xbootclasspath指定的jar包
ExtClassLoader 负责java.ext.dirs路径下类的加载,默认为jre/lib/ext或者-Djava.ext.dirs指定的jar包
AppClassLoader 负责加载java.class.path路径下类的加载,默认为classpath设定值下的jar包
CustomClassLoader自定义加载器。
每个加载器都有一个自己的命名空间,同一加载器名字相同的类只能存在一个,并且仅加载一次。java中,即使是同一个类文件,如果是由不同的类加载器实例加载的,他们的类型也是不相同的。
动态编译
public static void compiler(){
try {
String filePath = "D:\\IdeaProjects\\eventsource\\dynamicloading\\src\\main\\java\\HelloWorld.java";
String sourceDir = "D:\\IdeaProjects\\eventsource\\dynamicloading\\src\\main\\java";
String targetDir = "D:\\IdeaProjects\\eventsource\\dynamicloading\\src\\main\\resources\\swap";
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
boolean compilerResult = compiler(filePath, sourceDir, targetDir, diagnostics);
if (compilerResult) {
System.out.println("编译成功");
} else {
System.out.println("编译失败");
for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
// System.out.format("%s[line %d column %d]-->%s%n", diagnostic.getKind(), diagnostic.getLineNumber(),
// diagnostic.getColumnNumber(),
// diagnostic.getMessage(null));
System.out.println(diagnostic.getMessage(null));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 编译java文件
*
* @param filePath
* 文件或者目录(若为目录,自动递归编译)
* @param sourceDir
* java源文件存放目录
* @param targetDir
* 编译后class类文件存放目录
* @param diagnostics
* 存放编译过程中的错误信息
* @return
* @throws Exception
*/
public static boolean compiler(String filePath, String sourceDir, String targetDir, DiagnosticCollector<JavaFileObject> diagnostics)
throws Exception {
// 获取编译器实例
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 获取标准文件管理器实例
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
if (!Optional.ofNullable(filePath).isPresent() && !Optional.ofNullable(sourceDir).isPresent() && !Optional.ofNullable(targetDir).isPresent()) {
return false;
}
// 得到filePath目录下的所有java源文件
File sourceFile = new File(Objects.requireNonNull(filePath));
List<File> sourceFileList = new ArrayList<>();
getSourceFiles(sourceFile, sourceFileList);
// 没有java文件,直接返回
if (sourceFileList.size() == 0) {
System.out.println(filePath + "目录下查找不到任何java文件");
return false;
}
// 获取要编译的编译单元
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFileList);
/*
* 编译选项,在编译java文件时,编译程序会自动的去寻找java文件引用的其他的java源文件或者class。 -sourcepath选项就是定义java源文件的查找目录, -classpath选项就是定义class文件的查找目录。
*/
Iterable<String> options = Arrays.asList("-d", targetDir, "-sourcepath", sourceDir);
JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);
// 运行编译任务
return compilationTask.call();
}
}
/**
* 查找该目录下的所有的java文件
*
* @param sourceFile
* @param sourceFileList
* @throws Exception
*/
private static void getSourceFiles(File sourceFile, List<File> sourceFileList) throws Exception {
if (sourceFile.exists() && sourceFileList != null) {// 文件或者目录必须存在
if (sourceFile.isDirectory()) {// 若file对象为目录
// 得到该目录下以.java结尾的文件或者目录
File[] childrenFiles = sourceFile.listFiles(pathname -> {
if (pathname.isDirectory()) {
return true;
} else {
String name = pathname.getName();
return name.endsWith(".java");
}
});
// 递归调用
for (File childFile : Objects.requireNonNull(childrenFiles)) {
getSourceFiles(childFile, sourceFileList);
}
} else {// 若file对象为文件
sourceFileList.add(sourceFile);
}
}
}
自定义加载器
public class MyClassLoader extends ClassLoader {
private String basedir; // 需要该类加载器直接加载的类文件的基目录
private HashSet<String> dynaclazns; // 需要由该类加载器直接加载的类名
public MyClassLoader(String basedir, String[] clazns) throws IOException {
super(null); // 指定父类加载器为 null
this.basedir = basedir;
dynaclazns = new HashSet<>();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) throws IOException {
for (String clazn : clazns) {
loadDirectly(clazn);
dynaclazns.add(clazn);
}
}
private Class loadDirectly(String name) throws IOException {
Class cls = null;
StringBuilder sb = new StringBuilder(basedir);
String classname = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator).append(classname);
File classF = new File(sb.toString());
cls = instantiateClass(name,new FileInputStream(classF),classF.length());
return cls;
}
private Class<?> instantiateClass(String name, InputStream fin, long len) throws IOException {
byte[] raw = new byte[(int) len];
//noinspection ResultOfMethodCallIgnored
fin.read(raw);
fin.close();
return defineClass(name,raw,0,raw.length);
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if(!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}