使用Janino运行时动态编译Java源码并加载成Class使用

一、Janino简介

  Janino是一个超小、超快的开源Java 编译器。Janino不仅可以像javac一样将一组Java源文件编译成一组字节码class文件,还可以在内存中编译Java表达式、代码块、类和.java文件,加载字节码并直接在JVM中执行。
  有关Janino的更多简介请参考:Janino框架介绍。本文将介绍如何使用Janino在运行时动态编译Java源代码并加载成Class使用。

二、需求

  在运行时将已知的源代码编译成字节码并加载成Class使用。该Java源代码引用的相关类均能在当前运行的JVM中找到。

三、代码实现(基于JDK1.8)

相关代码可在gitee仓库中janino-demo获取。

1、pom中引入lombok及janino依赖

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.janino</groupId>
            <artifactId>janino</artifactId>
            <version>3.1.10</version>
        </dependency>

2、准备两个待编译的类源码,其中一个包含内部类

package com.shikx.demo.janino.compiler;

/**
 * @author Kaixuan Shi
 * @since 2023/7/17
 */
public class HelloServiceSource {

    public void sayHello() {
        System.out.println("Hello world!");
    }
}

package com.shikx.demo.janino.compiler;

/**
 * @author Kaixuan Shi
 * @since 2023/7/17
 */
public class OuterClassSource {

    public void sayHello(String name) {
        System.out.println("Hello, " + name + ", I'm parent class instance");
    }

    public class InnerClass {
        public void sayHello(String name) {
            System.out.println("Hello, " + name + ", I'm child class instance");
        }
    }
}

3、编写JavaSourceCode类,用于记录待编译的类名称及对应源码

import lombok.Data;

/**
 * @author Kaixuan Shi
 * @since 2023/7/17
 */
@Data
public class JavaSourceCode {
    /**
     * 类全名
     */
    private String fullClassName;
    /**
     * 源代码
     */
    private String sourceCode;
}

4、编写ClassLoaderService类,用于加载编译好的类

public class ClassLoaderService {

    private static ClassLoader classLoader;

    public static void setClassLoader(ClassLoader classLoader) {
        ClassLoaderService.classLoader = classLoader;
    }

    public static Class<?> loadClass(String fullClassName) throws ClassNotFoundException {
        return classLoader.loadClass(fullClassName);
    }
}

5、编写CompileService类,提供批量编译类方法

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.util.ResourceFinderClassLoader;
import org.codehaus.commons.compiler.util.resource.MapResourceCreator;
import org.codehaus.commons.compiler.util.resource.MapResourceFinder;
import org.codehaus.commons.compiler.util.resource.Resource;
import org.codehaus.commons.compiler.util.resource.StringResource;
import org.codehaus.janino.ClassLoaderIClassLoader;
import org.codehaus.janino.Compiler;
import org.codehaus.janino.CompilerFactory;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Kaixuan Shi
 * @since 2023/7/17
 */
public class CompileService {

    private static final CompilerFactory compilerFactory = new CompilerFactory();

    public void compile(List<JavaSourceCode> sourceCodes) {
        Compiler compiler = (Compiler) compilerFactory.newCompiler();
        Map<String, byte[]> classes = new HashMap<>();
        compiler.setClassFileCreator(new MapResourceCreator(classes));
        compiler.setIClassLoader(new ClassLoaderIClassLoader(CompileService.class.getClassLoader()));
        Resource[] resources = new Resource[sourceCodes.size()];
        for (int i = 0; i < sourceCodes.size(); i++) {
            JavaSourceCode sourceCode = sourceCodes.get(i);
            resources[i] = new StringResource(sourceCode.getFullClassName(), sourceCode.getSourceCode());
        }

        // 调用Janino编译服务
        try {
            compiler.compile(resources);
        } catch (CompileException | IOException e) {
            throw new RuntimeException(e);
        }

        // 将编译后的字节码class赋给ClassLoader,以使用此ClassLoader加载类
        ClassLoader cl = new ResourceFinderClassLoader(
                new MapResourceFinder(classes),
                CompileService.class.getClassLoader()
        );
        ClassLoaderService.setClassLoader(cl);
    }
}

  代码中“compiler.setIClassLoader(new ClassLoaderIClassLoader(CompileService.class.getClassLoader()));”这一步很关键,Janino编译时将使用该ClassLoader获取待编译源代码中依赖的相关类。若省略此步骤,Janino将从bootClassPath、classPath、extensionDirectories中寻找(参考org.codehaus.janino.Compiler#getIClassLoader),一些项目并不会将所有jar包放在classPath中,这将导致在运行时获取不到待编译源代码中依赖的类而报错。
  ResourceFinderClassLoader重写了
java.lang.ClassLoader#findClass方法,将从MapResourceFinder中获取待加载的类的字节码。

6、编写Demo,将2中准备好的两份类源码编译并加载,调用其方法


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Kaixuan Shi
 * @since 2023/7/17
 */
public class JaninoCompilerDemo {
    public static void main(String[] args) {
        //region 构建源代码
        JavaSourceCode helloServiceSourceCode = new JavaSourceCode();
        helloServiceSourceCode.setSourceCode("package org.csdn.qq_40820249.demo.janino.compiler;\n" +
                "\n" +
                "/**\n" +
                " * @author Kaixuan Shi\n" +
                " * @since 2023/7/17\n" +
                " */\n" +
                "public class HelloServiceSource {\n" +
                "\n" +
                "    public void sayHello() {\n" +
                "        System.out.println(\"Hello world!\");\n" +
                "    }\n" +
                "}");
        helloServiceSourceCode.setFullClassName("org.csdn.qq_40820249.demo.janino.compiler.HelloServiceSource");

        JavaSourceCode outerClassSourceCode = new JavaSourceCode();
        outerClassSourceCode.setSourceCode("package org.csdn.qq_40820249.demo.janino.compiler;\n" +
                "\n" +
                "/**\n" +
                " * @author Kaixuan Shi\n" +
                " * @since 2023/7/17\n" +
                " */\n" +
                "public class OuterClassSource {\n" +
                "\n" +
                "    public void sayHello(String name) {\n" +
                "        System.out.println(\"Hello, \" + name + \", I'm parent class instance\");\n" +
                "    }\n" +
                "\n" +
                "    public class InnerClass {\n" +
                "        public void sayHello(String name) {\n" +
                "            System.out.println(\"Hello, \" + name + \", I'm child class instance\");\n" +
                "        }\n" +
                "    }\n" +
                "}\n");
        outerClassSourceCode.setFullClassName("org.csdn.qq_40820249.demo.janino.compiler.OuterClassSource");

        List<JavaSourceCode> sourceCodes = new ArrayList<>(2);
        sourceCodes.add(helloServiceSourceCode);
        sourceCodes.add(outerClassSourceCode);
        //endregion

        //调用编译服务
        CompileService compileService = new CompileService();
        compileService.compile(sourceCodes);

        //通过反射调用编译好的类
        try {
            //调用helloServiceSourceCode中的类
            Class<?> helloServiceClass = ClassLoaderService.loadClass(helloServiceSourceCode.getFullClassName());
            Method helloServiceSayHelloMethod = helloServiceClass.getDeclaredMethod("sayHello");
            helloServiceSayHelloMethod.invoke(helloServiceClass.newInstance());
            //调用outerClassSourceCode中的“OuterClassSource”外部类
            Class<?> outerClass = ClassLoaderService.loadClass(outerClassSourceCode.getFullClassName());
            Method outerClassSayHelloMethod = outerClass.getDeclaredMethod("sayHello", String.class);
            outerClassSayHelloMethod.invoke(outerClass.newInstance(), "Janino");
            //调用outerClassSourceCode中的“InnerClass”内部类
            Class<?> innerClass = ClassLoaderService.loadClass(
                    "org.csdn.qq_40820249.demo.janino.compiler.OuterClassSource$InnerClass");
            Method innerClassChildClassSayHelloMethod = innerClass.getDeclaredMethod("sayHello", String.class);
            innerClassChildClassSayHelloMethod.invoke(
                    innerClass.getDeclaredConstructors()[0].newInstance(outerClass.newInstance()), "Janino");
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException |
                 InvocationTargetException e) {
            throw new RuntimeException(e);
        }
        System.out.println("完成Janino编译及调用演示");
    }
}

  这里有一点需要注意,就是内部类反射创建对象时,由于该内部类不是静态的,所以需要传入父类对象作为入参。
  执行结果如下:

Hello world!
Hello, Janino, I'm parent class instance
Hello, Janino, I'm child class instance
完成Janino编译及调用演示

四、总结

  以上代码简单演示了如何使用Janino框架编辑源代码并利用ClassLoader及Java反射技术调用编译好的类。实际应用场景一般要更复杂,可以针对不同的场景继续做封装,编译和加载类的核心就是这些。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐