什么是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;
    }
}