Bytecode enhancement

Posted by brian79 on Fri, 13 Dec 2019 16:34:21 +0100

  the previous section introduces the Java bytecode structure, and this section introduces the bytecode enhancement technology. Java bytecode enhancement refers to modifying and enhancing the function of Java bytecode after it is generated, which is equivalent to modifying the binary file of application program.

  common bytecode enhancement techniques include:

  • Java's own dynamic proxy
  • ASM
  • Javassist

1. Dynamic agent

  before introducing dynamic agents, introduce the agent mode. The following figure is the UML class diagram of agent mode:

Agent mode is a design mode, which provides additional access to the target object, that is, through the agent object to access the target object, so that it can provide additional functional operations and expand the function of the target object without modifying the original target object. Both proxy class ProxyAction and proxied class CoreActionImpl implement the same interface Action. ProxyAction also holds CoreActionImpl to call the core Action of proxied class. The implementation of agent pattern can be divided into static agent and dynamic agent.

1.1. Static agent

  static proxy is the literal translation of the above UML class diagram. If the proxy class already exists before the program runs, then this proxy method is called static proxy. In this case, the proxy class is usually defined in Java code. In general, the agent class and the delegate class in the static proxy will implement the same interface or derive from the same parent class, and then implement through aggregation, so that the proxy class can hold a reference of the delegate class. Examples are as follows:

public interface Action {
    void say();
}

public class CoreActionImpl implements Action {

    @Override
    public void say() {
        System.out.println("hello world");
    }
}

public class ProxyAction implements Action {
    private Action action = new CoreActionImpl();
    @Override
    public void say() {
        System.out.println("before core action");
        action.say();
        System.out.println("after core action");
    }
    
}
1.2. Dynamic agent

  the agent method created by the agent class when the program is running is called dynamic agent. In other words, in this case, the proxy class is not defined in Java code, but generated dynamically at run time. The dynamic proxy uses the JDK API. The dynamic proxy object does not need to implement the interface, but the target object must implement the interface, otherwise it cannot use the dynamic proxy. Examples are as follows:

public class DynamicProxyAction implements InvocationHandler {

    private Object obj;

    public DynamicProxyAction(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before core action");
        Object res = method.invoke(obj,args);
        System.out.println("after core action");
        return res;
    }
}

Call using the following method:

DynamicProxyAction proxyAction = new DynamicProxyAction(new CoreActionImpl());
Action action = (Action) Proxy.newProxyInstance(DynamicProxyAction.class.getClassLoader(),new Class[]{Action.class},proxyAction);
action.say()

The self-contained dynamic proxy implementation requires the proxy class to implement the InvocationHandler interface and complete the proxy process of the specific method in the method invoke.

  you can output proxy class content using the following

byte[] clazzData = ProxyGenerator.generateProxyClass(DynamicProxyAction.class.getCanonicalName() + "$Proxy0", new Class[]{Action.class});
OutputStream out = new FileOutputStream(DynamicProxyAction.class.getCanonicalName() + "$Proxy0" + ".class");
out.write(clazzData);
out.close();

The results are as follows:

public final class DynamicProxyAction$Proxy0 extends Proxy implements Action {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public DynamicProxyAction$Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void say() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("demo.Action").getMethod("say");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

As you can see, the proxy class inherits the java.lang.reflect.Proxy class and implements the proxy interface. In addition to the methods with proxy interface, there are three additional methods equal, toString and hashCode in the proxy class. The internal implementation of all methods is delegated to the InvocationHandler, which is the incoming DynamicProxyAction.

1.3. Cglib

A very obvious requirement of java's own dynamic proxy is that the class being proxied has an implementation interface, and Cglib proxy exists to solve this problem. CGLIB (Code Generator Library) is a powerful and high-performance code generation library. It is widely used in AOP framework (Spring, dynaop) to provide methods to intercept operations. ASM is used to manipulate bytecode to generate new classes.

  Cglib is mainly to dynamically generate a subclass of the class to be proxy, which overrides all non final methods of the class to be proxy. In the subclass, the technology of method interception is used to intercept the calls of all the parent methods, and the crosscutting logic is weaved into the subclass, so the final method cannot be represented.

  to achieve the above proxy effect on CoreActionImpl, you can implement it as follows:

public class CglibProxyAction {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CoreActionImpl.class);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("before core action");
                Object res = methodProxy.invokeSuper(o, objects);
                System.out.println("after core action");
                return res;
            }
        });
        CoreActionImpl action = (CoreActionImpl) enhancer.create();
        action.say();
    }
}

You can open Cglib's debug parameter Cglib.debuglocation (debuggingclasswriter. Debug "location" property) to output the class object after the agent. Because the content is too long, not all of them are posted here, but only the core part, as follows:

public final void say() {
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (var10000 == null) {
        CGLIB$BIND_CALLBACKS(this);
        var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) {
        var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);
    } else {
        super.say();
    }
}

As mentioned above, the generated subclass overrides the parent method and intercepts it.

2. ASM

 ASM(https://asm.ow2.io/ )Is a Java bytecode control framework. It can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can generate binary class files directly or change the behavior of classes before they are loaded into Java virtual machine.

The ASM tool provides two ways to generate and transform compiled class files, which are event based and object-based representation models. The event based representation model is similar to SAX's handling of XML. It uses an orderly sequence of events to represent a class file. Each element in the class file is represented by an event, such as the class header, variables, and method declaration. JVM instructions have corresponding event representations. ASM uses its own event parser to parse each class file into an event sequence. The object-based representation model is similar to DOM processing XML, which uses the object tree structure to parse each file. The following is the sequence diagram describing the event model found on the Internet, which clearly introduces the processing flow of ASM:

There are many introductions about ASM on the official website and the Internet, which will not be covered here. Here is an example.

For the CoreActionImpl mentioned above, its bytecode content is as follows:

public class demo/CoreActionImpl implements demo/Action  {

  // compiled from: CoreActionImpl.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Ldemo/CoreActionImpl; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public say()V
   L0
    LINENUMBER 7 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello world"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 8 L1
    RETURN
   L2
    LOCALVARIABLE this Ldemo/CoreActionImpl; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

If we want to intercept the say method and add the front and back logs, we need to add the corresponding bytecode at the entry and return of the method, which can be realized by using the following code:

public class ASMProxyAction {

    public static void main(String[] args) throws IOException {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassReader cr = new ClassReader(Thread.currentThread().getContextClassLoader().getResourceAsStream("demo/CoreActionImpl.class"));
        cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                if (!"say".equals(name)) {
                    return mv;
                }
                MethodVisitor aopMV = new MethodVisitor(super.api, mv) {
                    @Override
                    public void visitCode() {
                        super.visitCode();
                        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        mv.visitLdcInsn("before core action");
                        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    }

                    @Override
                    public void visitInsn(int opcode) {
                        if (Opcodes.RETURN == opcode) {
                            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                            mv.visitLdcInsn("after core action");
                            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                        }
                        super.visitInsn(opcode);
                    }
                };
                return aopMV;
            }
        }, ClassReader.SKIP_DEBUG);
        File file = new File("CoreActionImpl.class");
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(cw.toByteArray());
        fos.close();
    }
}

This code rewrites the visitCode and visitInsn(Opcodes.RETURN == opcode) of MethodVisitor and increases the corresponding logs.

The modified bytecode is as follows:

public class demo/CoreActionImpl implements demo/Action  {


  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public say()V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "before core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello world"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "after core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 1
}

3. Javassist

  Javassist is a dynamic class library that can be used to check, dynamically modify, and create Java classes. Its function is similar to the reflection function of jdk, but more powerful than the reflection function. Compared with ASM, Javassist directly uses the form of java coding, without knowing the instructions of virtual machine, it can change the structure of class or generate class dynamically. More content can be found on the official website http://www.javassist.org/ , the following is a direct example.

  to achieve the above proxy effect on CoreActionImpl, you can implement it as follows:

public class JavassistProxyAction {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("demo.CoreActionImpl");
        CtMethod methodSay = cc.getDeclaredMethod("say");
        methodSay.insertBefore("System.out.println(\"before core action\");");
        methodSay.insertAfter("System.out.println(\"after core action\");");
        File file = new File("CoreActionImpl.class");
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(cc.toBytecode());
        fos.close();
    }

}

The above code is more intuitive. It obtains the target method directly, and then inserts the target logic before and after the method, which is Java code. The content of the class decompiled generated by the above code is as follows:

public class demo/CoreActionImpl implements demo/Action  {

  // compiled from: CoreActionImpl.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Ldemo/CoreActionImpl; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public say()V
   L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "before core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 7 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello world"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 8 L2
    GOTO L3
   L3
   FRAME SAME
    ACONST_NULL
    ASTORE 2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "after core action"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
    LOCALVARIABLE this Ldemo/CoreActionImpl; L0 L3 0
    MAXSTACK = 5
    MAXLOCALS = 3
}

More original content, please search WeChat public number: doubaotaizi

Topics: Java JDK xml Spring