字節碼加強

 上節介紹了Java字節碼結構,這節介紹字節碼加強技術。Java字節碼加強指的是在Java字節碼生成以後,對其進行修改,加強其功能,這種方式至關於對應用程序的二進制文件進行修改。java

 常見的字節碼加強技術包括:git

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

1. 動態代理

 在介紹動態代理前,先介紹代理模式。以下圖,爲代理模式的UML類圖:github

file

代理模式是一種設計模式,提供了對目標對象額外的訪問方式,即經過代理對象訪問目標對象,這樣能夠在不修改原目標對象的前提下,提供額外的功能操做,擴展目標對象的功能。代理類ProxyAction和被代理類CoreActionImpl都實現了同一個接口Action,ProxyAction還持有了CoreActionImpl,以調用被代理類的核心操做。代理模式實現能夠分爲靜態代理和動態代理。設計模式

1.1. 靜態代理

 靜態代理徹底就是上面UML類圖的直譯,若代理類在程序運行前就已經存在,那麼這種代理方式被成爲 靜態代理 ,這種狀況下的代理類一般都是咱們在Java代碼中定義的。 一般狀況下, 靜態代理中的代理類和委託類會實現同一接口或是派生自相同的父類,再經過聚合來實現,讓代理類持有一個委託類的引用便可。例子以下:api

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中完成具體方法的代理過程。ide

 能夠使用以下內容輸出代理類內容工具

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的處理流程:

file

官網和網上有不少關於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)

相關文章
相關標籤/搜索