1. 程式人生 > >位元組碼增強

位元組碼增強

 上節介紹了Java位元組碼結構,這節介紹位元組碼增強技術。Java位元組碼增強指的是在Java位元組碼生成之後,對其進行修改,增強其功能,這種方式相當於對應用程式的二進位制檔案進行修改。

 常見的位元組碼增強技術包括:

  • Java自帶的動態代理
  • ASM
  • Javassist

1. 動態代理

 在介紹動態代理前,先介紹代理模式。如下圖,為代理模式的UML類圖:

代理模式是一種設計模式,提供了對目標物件額外的訪問方式,即通過代理物件訪問目標物件,這樣可以在不修改原目標物件的前提下,提供額外的功能操作,擴充套件目標物件的功能。代理類ProxyAction和被代理類CoreActionImpl都實現了同一個介面Action,ProxyAction還持有了CoreActionImpl,以呼叫被代理類的核心操作。代理模式實現可以分為靜態代理和動態代理。

1.1. 靜態代理

 靜態代理完全就是上面UML類圖的直譯,若代理類在程式執行前就已經存在,那麼這種代理方式被成為 靜態代理 ,這種情況下的代理類通常都是我們在Java程式碼中定義的。 通常情況下, 靜態代理中的代理類和委託類會實現同一介面或是派生自相同的父類,再通過聚合來實現,讓代理類持有一個委託類的引用即可。例子如下:

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. 動態代理

 代理類在程式執行時建立的代理方式被稱為動態代理。也就是說,這種情況下,代理類並不是在Java程式碼中定義的,而是在執行時動態生成的。動態代理利用了JDK API,動態代理物件不需要實現介面,但是要求目標物件必須實現介面,否則不能使用動態代理。例子如下:

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

使用如下方法進行呼叫:

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

自帶的動態代理實現要求代理類實現InvocationHandler介面,並在方法invoke中完成具體方法的代理過程。

 可以使用如下內容輸出代理類內容

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

得到內容如下:

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

可以看到,代理類繼承了java.lang.reflect.Proxy類並實現了代理介面。代理類內部除了有代理介面的方法外還額外增加了3個方法equals、toString和hashCode。所有方法的內部實現都委託給了InvocationHandler來執行,該InvocationHandler為傳入的DynamicProxyAction。

1.3. Cglib

 java自帶的動態代理一個很明顯的要求就是被代理類有實現介面,Cglib代理則是為了解決這個問題而存在的。CGLIB(Code Generator Library)是一個強大的、高效能的程式碼生成庫。其被廣泛應用於AOP框架(Spring、dynaop)中,用以提供方法攔截操作,其底層使用了ASM來操作位元組碼生成新的類。

 Cglib主要是通過動態生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯,對於final方法,無法進行代理。

 為了實現上面對CoreActionImpl的代理效果,可以如下實現:

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

可以開啟Cglib的debug引數cglib.debugLocation(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY),輸出代理後的class物件,由於內容過長,這裡不全部貼出,只貼出核心部分,如下:

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

如上說的,生成的子類重寫了父類方法,並進行了攔截。

2. ASM

 ASM(https://asm.ow2.io/ )是一個Java位元組碼操控框架。它可以被用來動態生成類或者增強既有類的功能。ASM可以直接產生二進位制的class檔案,也可以在類被載入到Java虛擬機器之前改變類行為。

 ASM工具提供兩種方式來產生和轉換已編譯的class檔案,它們分別是基於事件和基於物件的表示模型。其中,基於事件的表示模型的方式類似於 SAX 處理XML。它使用一個有序的事件序列表示一個class檔案,class檔案中的每一個元素使用一個事件來表示,比如class的頭部,變數,方法宣告JVM指令都有相對應的事件表示,ASM使用自帶的事件解析器能將每一個class檔案解析成一個事件序列。而基於物件的表示模型則類似於DOM處理XML,其使用物件樹結構來解析每一個檔案。如下為從網上找到的描述事件模型的時序圖,比較清晰的介紹了ASM的處理流程:

官網和網上有很多關於ASM的介紹,這邊不再贅述,下面給出例子說明。

針對上面提到的CoreActionImpl,它的位元組碼內容如下:

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
}

我們要攔截say方法,增加前後日誌,則需要在方法的入口處以及返回處增加對應的位元組碼,可以使用如下的程式碼實現:

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

該程式碼重寫了MethodVisitor的visitCode和visitInsn(Opcodes.RETURN == opcode),增加了對應的日誌。

修改後的位元組碼如下:

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是一個動態類庫,可以用來檢查、”動態”修改以及建立 Java類。其功能與jdk自帶的反射功能類似,但比反射功能更強大。相比較ASM,Javassist直接使用java編碼的形式,而不需要了解虛擬機器指令,就能動態改變類的結構,或者動態生成類。更多內容可以看官網http://www.javassist.org/,下面直接講解例子。

 為了實現上面對CoreActionImpl的代理效果,可以如下實現:

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

}

上面的程式碼比較直觀,直接獲得目標方法,然後在該方法前後插入了目標邏輯,且為Java程式碼。上面程式碼生成的class反編譯後的內容如下:

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
}

更多原創內容請搜尋微信公眾號:啊駝(doubaotaizi)