Java动态编译和热更新

在日常开发过程中,我们经常遇到临时开发一些额外的功能(需要在Test接口中手动调用),每次都必须重新提交代码,打包发布,无疑费时费力。

那么有什么方法可以跳过繁琐的打包过程呢?

答案是有的,Java 从6开始提供了动态编译API

Java Compiler

Java Compiler API,这是JDK6开始提供的标准API,提供了与javac对等的编译功能,即动态编译,文档地址

步骤
  1. 通过 Controller 接口,提交Java代码,代码统一实现Callable接口;
  2. 动态编译Java代码为字节码;
  3. 使用类加载器加载编译的字节码;
  4. 使用反射拿到类的元数据信息,执行call方法,完成对应的功能。
代码实现
public class ClassGenerator {

    private String classRootDir;

    public ClassGenerator() {
        this(".");
    }

    public ClassGenerator(String classRootDir) {
        this.classRootDir = classRootDir;
    }


    public Class<?> generate(String classFullName, String code, String jarFile) throws MalformedURLException, ClassNotFoundException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileObject fileObject = new JavaSourceFromString(classFullName, code);

        File root = new File(classRootDir);
        if (!root.exists()) {
            root.mkdirs();
        }

        String jars = getJars(jarFile);
        Iterable<String> options = Arrays.asList("-d", classRootDir, "-cp", jars + File.pathSeparator + classRootDir);
        Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(fileObject);
        JavaCompiler.CompilationTask task = compiler.getTask(null, null, null, options, null, compilationUnits);

        // 动态编译
        boolean success = task.call();
        if (!success) {
            return null;
        }
        SLogger.info("compile success root path = " + root.getPath());
        URL[] urls = new URL[]{root.toURI().toURL()};
        // 设置父类加载器
        URLClassLoader classLoader = new URLClassLoader(urls, ClassGenerator.class.getClassLoader());
        return Class.forName(classFullName, true, classLoader);
    }

    private String getJars(String jarFile) {
        File file = new File(jarFile);
        if (!file.exists()) {
            return "";
        }

        StringBuilder builder = new StringBuilder();
        for (File jar : Objects.requireNonNull(file.listFiles())) {
            builder.append(jar.getPath()).append(File.pathSeparator);
        }

        return builder.toString();
    }
}
复制代码
public class JavaSourceFromString extends SimpleJavaFileObject {

    private String code;

    JavaSourceFromString(String name, String code) {
        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
        this.code = code;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return code;
    }
}
复制代码
public class DynamicCompile {

    public static Object compileInvoke(String code, String classFullName) throws Throwable {
        String root = DynamicCompile.class.getClassLoader().getResource("").getPath();
        String jarFile = root.replace("/classes", "/lib");

        ClassGenerator builder = new ClassGenerator(root);
        Class<?> cls = builder.generate(classFullName, code, jarFile);
        Object instance = cls.newInstance();
        if (!(instance instanceof Callable)) {
            throw new RuntimeException("only support Callable");
        }

        MethodType methodType = MethodType.methodType(Object.class);
        MethodHandle methodHandle = MethodHandles.lookup().findVirtual(cls, "call", methodType);
        return methodHandle.invoke(instance);
    }
  
}

复制代码
@RestController
@RequestMapping("/backend")
public class ExtendController {

    @PostMapping("/extend")
    public JSONResult executeByCode(@RequestParam("code") String code,
                                    @RequestParam("class") String cls) {
        if (StringUtils.isBlank(code)) {
            return JSONResult.paramErrorResult("code is null");
        }
        if (StringUtils.isBlank(cls)) {
            return JSONResult.paramErrorResult("class is null");
        }
        try {
            Object invoke = DynamicCompile.compileInvoke(code, cls);
            return JSONResult.okResult(invoke);
        } catch (Exception e) {
            SLogger.error(e.getMessage(), e);
            return JSONResult.failureResult(e.getMessage());
        }
    }

    /**
     * Java 文件提交
     * @param java
     * @param cls
     * @return
     * @throws IOException
     */
    @PostMapping("/extend/java")
    public JSONResult executeByJava(MultipartFile java,
                             @RequestParam("class") String cls) throws IOException {
        if (java == null) {
            return JSONResult.paramErrorResult("java is null");
        }
        if (StringUtils.isBlank(cls)) {
            return JSONResult.paramErrorResult("class is null");
        }
        byte[] bytes = java.getBytes();
        String code = new String(bytes);
        try {
            Object invoke = DynamicCompile.compileInvoke(code, cls);
            return JSONResult.okResult(invoke);
        } catch (Exception e) {
            SLogger.error(e.getMessage(), e);
            return JSONResult.failureResult(e.getMessage());
        }
    }

}
复制代码
热更新

这样基本上可以实现我们的需求了,但是如果想要实现通过替换class文件来动态修复线上Bug,会发现并没有生效,执行的还是原来的结果,这是因为 Java 的类加载是存在缓存的,代码有删减:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    // 先检查缓存是否加载过
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        // 未加载过委托父类加载
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name);
        }
        // 加载类信息
        if (c == null) {
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
复制代码

有什么办法可以跳过缓存呢?是否可以主动卸载类呢?Java 没有提供对应的API,只能通过自定义类加载器实现。

思路:通过 Map 缓存类的最新修改时间,每次加载的时候检查 class 文件的修改时间是否和缓存一致,不一样则重新加载。

类生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  • 该类所有的实例都已经被GC。
  • 加载该类的ClassLoader实例已经被GC。
  • 该类的java.lang.Class对象没有在任何地方被引用。
类加载机制

Java 类加载机制是“双亲委派”模型,即优先让父ClassLoader去加载。

Java 默认使用以下三种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,我们日常用的Java类库比如String、ArrayList等都位于该包内。
  2. 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Laun-cher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包。
  3. 应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc. Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。

这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader, Extension的父亲是Bootstrap ClassLoader。注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子Class-Loader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:

  1. 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个Class-Loader加载一次。
  2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
  3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类。

注意,不同类加载器加载同一类的得到的Class对象是不同的,Tomcat 通过 WebappClassLoader 隔离不同的web应用。

需要说明的是,Java 9引入了模块的概念。在模块化系统中,类加载的过程有一些变化,扩展类的目录被删除掉了,原来的扩展类加载器没有了,增加了一个平台类加载器(Platform Class Loader),角色类似于扩展类加载器,它分担了一部分启动类加载器的职责,另外,加载的顺序也有一些变化。

总结:

使用动态编译可以在不打包编译重启整个应用的情况下,实现功能的扩展,结合自定义类加载器,可以替换已加载过的类,实现线上Bug的修复,不过需要替换Tomcat 默认的类加载器 WebappClassLoader 此外不适合API这样的多节点,需要其他手段来保证代码提交到各个节点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值