Java runtime dynamically generates classes

1. It is theoretically feasible to create bytecode from scratch, but it is actually very difficult

2. CGLib (the code is hard to understand)

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();

3. java built-in compiler

(1) Read java files directly

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

The problem is that after we create Java code in memory, we must first write to the file, then compile, and finally manually read the contents of the class file and load it with a ClassLoader.

(2) The Compiler compiles 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:

  • Replace the JDK default standard Java FileManager with MemoryJavaFileManager, so that when the compiler requests the source code content, it will not read from the file, but directly return String;
  • 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 saved in memory.

Finally, the compiled results are placed in map < string, byte [] >. 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 of one java source files may have multiple after compilation Class file! As long as static classes and anonymous classes are included, there must be more than one compiled class.

(3) Load compiled classes

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);
  }
}

Load using your own class loader

4,com.itranswarp.compiler implementation

5. Using Groovy scripts

6. Javassist implementation

The most important classes in Javassist are ClassPool, CtClass, CtMethod, and CtField.

ClassPool: a container of CtClass objects implemented based on HashMap. The key is the class name and the value is the CtClass object representing the class. The default ClassPool uses the same classpath as the underlying JVM, so in some cases, you may need to add Classpaths or classbytes to the ClassPool.

CtClass: represents a class. These CtClass objects can be obtained from ClassPool.

CtMethods: represents the methods in the class.

CtFields: represents the fields in the class.

Use examples

 public void DynGenerateClass() {
     ClassPool pool = ClassPool.getDefault();
     CtClass ct = pool.makeClass("top.ss007.GenerateClass");//Create class
     ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//Let the class implement the clonable interface
     try {
         CtField f= new CtField(CtClass.intType,"id",ct);//Get a field of type int and name id
         f.setModifiers(AccessFlag.PUBLIC);//Set the field to public
         ct.addField(f);//Set field on class
         //Add constructor
         CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct);
         ct.addConstructor(constructor);
         //Add method
         CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct);
         ct.addMethod(helloM);

         ct.writeFile();//To be generated Save class file to disk

         //The following code is the validation code
         Field[] fields = ct.toClass().getFields();
         System.out.println("Attribute name:" + fields[0].getName() + "  Attribute type:" + fields[0].getType());
     } catch (CannotCompileException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     } catch (NotFoundException e) {
         e.printStackTrace();
     }
 }

Dynamic modification method body (AOP)

We focus on dynamically modifying the content of a method. For example, in AOP programming, we will use this technology to dynamically insert code into a method.  

public class Point {
    private int x;
    private int y;

    public Point(){}
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void move(int dx, int dy) {
        this.x += dx;
        this.y += dy;
    }
}

Add content to the move method

    public void modifyMethod()
    {
        ClassPool pool=ClassPool.getDefault();
        try {
            CtClass ct=pool.getCtClass("top.ss007.Point");
            CtMethod m=ct.getDeclaredMethod("move");
            m.insertBefore("{ System.out.print(\"dx:\"+$1); System.out.println(\"dy:\"+$2);}");
            m.insertAfter("{System.out.println(this.x); System.out.println(this.y);}");

            ct.writeFile();
            //Call the method through reflection to view the results
            Class pc=ct.toClass();
            Method move= pc.getMethod("move",new Class[]{int.class,int.class});
            Constructor<?> con=pc.getConstructor(new Class[]{int.class,int.class});
            move.invoke(con.newInstance(1,2),1,2);
        }
        ...
    }

result:

  public void move(int dx, int dy) {
    System.out.print("dx:" + dx);System.out.println("dy:" + dy);
    this.x += dx;
    this.y += dy;
    Object localObject = null;//Method return value
    System.out.println(this.x);System.out.println(this.y);
  }

reference resources:

1-5: Detailed explanation of Java runtime dynamic generation class implementation process

6: Javassist Chinese technical documentation  | Official website (google translate)|  Second understand Java dynamic programming (Javassist Research)

Keywords: Java C#

Added by ryanbutler on Sun, 19 Dec 2021 02:51:45 +0200