[java] Several ways to dynamically generate classes at runtime in Java

insert image description here

1 Overview

Reprinted: Several ways to dynamically generate classes at runtime in Java

Here I found that I didn't know, it turned out that Java can compile itself, and I learned it.

In a recent project, a rule engine is used to provide users with flexible and drag-and-drop definition rules. This requires the logic of dynamically generating objects based on database data to process specific rules. If you write by hand not only to modify the code every time, but also to test the release every time, and you cannot flexibly process logic dynamically according to user-defined rules. So I thought of writing the common logic to the parent class implementation, and dynamically generating subclasses for specific logic based on strings. This solves the problem once and for all.

Then start with how Java dynamically generates objects at runtime based on string templates.

Java is a static language. Usually, the classes we need are already generated at compile time. Why do we sometimes want to dynamically generate classes at runtime?

After some online information search, the way from complex to simple is summarized as follows:

2. Use JDK's own tool class to achieve

Now the question is, how difficult is it to dynamically generate bytecode?
  If we want to directly output the bytecode in binary format, before completing this task, we must read Chapter 4 of the JVM specification carefully to understand the class file structure in detail. It is estimated that after reading the specification, two months have passed.
  So, the first method, do it yourself, create bytecode from scratch, which is theoretically possible, but difficult in practice.
  The second method is to use some existing libraries that can manipulate bytecode to help us create classes.
  At present, there are mainly two open source libraries that can manipulate bytecodes, CGLib and Javassist, both of which provide relatively high-level APIs to manipulate bytecodes, and finally output them as class files.
For example, CGLib, the typical usage is as follows:

Enhancer e = new Enhancer();
e.setSuperclass(...);
e.setStrategy(new DefaultGeneratorStrategy() {
    
    
    protected ClassGenerator transform(ClassGenerator cg) {
    
    
        return new TransformingGenerator(cg,
            new AddPropertyTransformer(new String[]{
    
     "foo" },
                    new Class[] {
    
     Integer.TYPE }));
    }});
Object obj = e.create();

It is simpler than generating the class yourself, but it still takes a lot of time to learn its API, and the above code is difficult to understand, right?

Is there an easier way?

have!

Java's compiler is javac. However, Java's compiler has been rewritten in pure Java since a long time ago. It can compile itself. The industry slang is "bootstrapping". Since Java 1.6, the compiler interface has been officially put into the public API of JDK, so we do not need to create a new process to call javac, but directly use the compiler API to compile the source code.

It is also very simple to use:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, '/path/Test.java');

There is nothing wrong with writing and compiling in this way. The problem is that after we create the Java code in memory, we must first write it to the file, then compile it, and finally manually read the content of the class file and load it with a ClassLoader.

Is there an easier way?

have!

In fact, the Java compiler doesn't care where the content of the source code comes from. You give it a String as the source code, and it can output byte[] as the content of the class.

Therefore, we need to refer to the documentation of the Java Compiler API, let the Compiler complete the compilation directly in memory, and the output class content is byte[].

Map<String, byte[]> results;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
    
    
    JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
    CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
    if (task.call()) {
    
    
        results = manager.getClassBytes();
    }
}

The key points of the above code are:

  1. Replace JDK's default StandardJavaFileManager with MemoryJavaFileManager, so that when the compiler requests source content, it does not read from a file, but directly returns a String;

  2. Replace the JDK default SimpleJavaFileObject with MemoryOutputJavaFileObject, so that when the byte[] content generated by the compiler is received, the class file is not written, but directly stored in memory.

Finally, the compiled result is placed in Map<String, byte[]>, the Key is the class name, and the corresponding byte[] is the binary content of the class.

Why is it not a byte[] after compilation?

Because a .java source file may have multiple .class files after compilation! As long as static classes, anonymous classes, etc. are included, there must be more than one class compiled.

How to load the compiled class?

Loading a class is relatively easy, we just need to create a ClassLoader and override the findClass() method:

class MemoryClassLoader extends URLClassLoader {
    
    

    Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

    public MemoryClassLoader(Map<String, byte[]> classBytes) {
    
    
        super(new URL[0], MemoryClassLoader.class.getClassLoader());
        this.classBytes.putAll(classBytes);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        byte[] buf = classBytes.get(name);
        if (buf == null) {
    
    
            return super.findClass(name);
        }
        classBytes.remove(name);
        return defineClass(name, buf, 0, buf.length);
    }
}

To sum up the above, let's write a Java scripting engine:

https://github.com/barrywang88/compiler

3. Use the three-party Jar package to achieve

Use the three-party package com.itranswarp.compiler to achieve:

  1. Introduce Maven dependencies:
<dependency>
    <groupId>com.itranswarp</groupId>
    <artifactId>compiler</artifactId>
    <version>1.0</version>
</dependency>
  1. write tool class
public class StringCompiler {
    
    
    public static Object run(String source, String...args) throws Exception {
    
    
        // 声明类名
        String className = "Main";
        String packageName = "top.fomeiherz";
        // 声明包名:package top.fomeiherz;
        String prefix = String.format("package %s;", packageName);
        // 全类名:top.fomeiherz.Main
        String fullName = String.format("%s.%s", packageName, className);
        
        // 编译器
        JavaStringCompiler compiler = new JavaStringCompiler();
        // 编译:compiler.compile("Main.java", source)
        Map<String, byte[]> results = compiler.compile(className + ".java", prefix + source);
        // 加载内存中byte到Class<?>对象
        Class<?> clazz = compiler.loadClass(fullName, results);
        // 创建实例
        Object instance = clazz.newInstance();
        Method mainMethod = clazz.getMethod("main", String[].class);
        // String[]数组时必须使用Object[]封装
        // 否则会报错:java.lang.IllegalArgumentException: wrong number of arguments
        return mainMethod.invoke(instance, new Object[]{
    
    args});
    }
}
  1. test execution
public class StringCompilerTest {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 传入String类型的代码
        String source = "import java.util.Arrays;public class Main" +
                "{" +
                "public static void main(String[] args) {" +
                "System.out.println(Arrays.toString(args));" +
                "}" +
                "}";
        StringCompiler.run(source, "1", "2");
    }
}

4. Using Groovy script to achieve

I tried the above two methods, and later found that Groovy natively supports scripts to dynamically generate objects.

  1. Introduce Groovy maven dependencies
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.13</version>
  </dependency>
  1. Go directly to the test code
@Test
    public void testGroovyClasses() throws Exception {
    
    
        //groovy提供了一种将字符串文本代码直接转换成Java Class对象的功能
        GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
        //里面的文本是Java代码,但是我们可以看到这是一个字符串我们可以直接生成对应的Class<?>对象,而不需要我们写一个.java文件
        Class<?> clazz = groovyClassLoader.parseClass("package com.xxl.job.core.glue;\n" +
                "\n" +
                "public class Main {\n" +
                "\n" +
                "    public int age = 22;\n" +
                "    \n" +
                "    public void sayHello() {\n" +
                "        System.out.println(\"年龄是:\" + age);\n" +
                "    }\n" +
                "}\n");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sayHello");
        method.invoke(obj);

        Object val = method.getDefaultValue();
        System.out.println(val);
    }

5. Using Scala script to achieve

  1. Define tool class
package com.damll.rta.flink.utils
 
import java.lang.reflect.Method
import java.util
import java.util.UUID
import scala.reflect.runtime.universe
import scala.tools.reflect.ToolBox
 
case class ClassInfo(clazz: Class[_], instance: Any, defaultMethod: Method, methods: Map[String, Method]) {
    
    
  def invoke[T](args: Object*): T = {
    
    
    defaultMethod.invoke(instance, args: _*).asInstanceOf[T]
  }
}
 
object ClassCreateUtils {
    
    
  private val clazzs = new util.HashMap[String, ClassInfo]()
  private val classLoader = scala.reflect.runtime.universe.getClass.getClassLoader
  private val toolBox = universe.runtimeMirror(classLoader).mkToolBox()
 
  def apply(classNameStr: String, func: String): ClassInfo = this.synchronized {
    
    
    var clazz = clazzs.get(func)
    if (clazz == null) {
    
    
      val (className, classBody) = wrapClass(classNameStr, func)
      val zz = compile(prepareScala(className, classBody))
      val defaultMethod = zz.getDeclaredMethods.head
      val methods = zz.getDeclaredMethods
      clazz = ClassInfo(
        zz,
        zz.newInstance(),
        defaultMethod,
        methods = methods.map {
    
     m => (m.getName, m) }.toMap
      )
      clazzs.put(func, clazz)
    }
    clazz
  }
 
  def compile(src: String): Class[_] = {
    
    
    val tree = toolBox.parse(src)
    toolBox.compile(tree).apply().asInstanceOf[Class[_]]
  }
 
  def prepareScala(className: String, classBody: String): String = {
    
    
    classBody + "\n" + s"scala.reflect.classTag[$className].runtimeClass"
  }
 
  def wrapClass(className:String, function: String): (String, String) = {
    
    
    //val className = s"dynamic_class_${UUID.randomUUID().toString.replaceAll("-", "")}"
    val classBody =
      s"""
         |class $className extends Serializable{
    
    
         |  $function
         |}
            """.stripMargin
    (className, classBody)
  }
}
  1. Invoke dynamically loaded classes
object CreateTest {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val cim = ClassCreateUtils("Calculator", "def toUps(str:String):String = str.toUpperCase")
    val value = cim.methods("toUps").invoke(cim.instance, "hello")
    println(value) // method1
    println(cim.invoke("World")) // method2
  }
}
  1. operation result
HELLO
WORLD

6. Using Aviator script to achieve

  1. import jar

    com.googlecode.aviator aviator 4.2.10
  2. Write code

copy code

@Test
    public void testAviatorClasses() throws Exception {
    
    
        final ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // AviatorScript code in a String. This code defines a script object 'obj'
        // with one method called 'hello'.
        String script =
                "var obj = new Object(); obj.hello = function(name) { print('Hello, ' + name); }";
        // evaluate script
        engine.eval(script);

        // javax.script.Invocable is an optional interface.
        // Check whether your script engine implements or not!
        // Note that the AviatorScript engine implements Invocable interface.
        Invocable inv = (Invocable) engine;

        // get script object on which we want to call the method
        Object obj = engine.get("obj");

        // invoke the method named "hello" on the script object "obj"
        inv.invokeMethod(obj, "hello", "Script Method !!");
    }

copy code

See: https://docs.oracle.com/javase/7/docs/technotes/guides/scripting/programmer_guide/#helloworld

Guess you like

Origin blog.csdn.net/qq_21383435/article/details/123548446