秒懂Java動態編程(Javassist研究)

[toc]java

概述

什麼是動態編程?動態編程解決什麼問題?Java中如何使用?什麼原理?如何改進?(須要咱們一塊兒探索,因爲本身也是比較菜,通常深刻不到這個程度)。編程

什麼是動態編程

動態編程是相對於靜態編程而言的,平時咱們討論比較多的就是靜態編程語言,例如Java,與動態編程語言,例如JavaScript。那兩者有什麼明顯的區別呢?簡單的說就是在靜態編程中,類型檢查是在編譯時完成的,而動態編程中類型檢查是在運行時完成的。所謂動態編程就是繞過編譯過程在運行時進行操做的技術,在Java中有以下幾種方式:服務器

反射

這個搞Java的應該比較熟悉,原理也就是經過在運行時得到類型信息而後作相應的操做。框架

動態編譯

動態編譯是從Java 6開始支持的,主要是經過一個JavaCompiler接口來完成的。經過這種方式咱們能夠直接編譯一個已經存在的java文件,也能夠在內存中動態生成Java代碼,動態編譯執行。編程語言

調用JavaScript引擎

Java 6加入了對Script(JSR223)的支持。這是一個腳本框架,提供了讓腳本語言來訪問Java內部的方法。你能夠在運行的時候找到腳本引擎,而後調用這個引擎去執行腳本。這個腳本API容許你爲腳本語言提供Java支持。函數

動態生成字節碼

這種技術經過操做Java字節碼的方式在JVM中生成新類或者對已經加載的類動態添加元素。工具

動態編程解決什麼問題

在靜態語言中引入動態特性,主要是爲了解決一些使用場景的痛點。其實徹底使用靜態編程也辦的到,只是付出的代價比較高,沒有動態編程來的優雅。例如依賴注入框架Spring使用了反射,而Dagger2 卻使用了代碼生成的方式(APT)。this

例如 1: 在那些依賴關係須要動態確認的場景: 2: 須要在運行時動態插入代碼的場景,好比動態代理的實現。 3: 經過配置文件來實現相關功能的場景編碼

Java中如何使用

此處咱們主要說一下經過動態生成字節碼的方式,其餘方式能夠自行查找資料。.net

操做java字節碼的工具備兩個比較流行,一個是ASM,一個是Javassit 。

ASM :直接操做字節碼指令,執行效率高,要是使用者掌握Java類字節碼文件格式及指令,對使用者的要求比較高。

Javassit 提供了更高級的API,執行效率相對較差,但無需掌握字節碼指令的知識,對使用者要求較低。

應用層面來說通常使用建議優先選擇Javassit,若是後續發現Javassit 成爲了整個應用的效率瓶頸的話能夠再考慮ASM.固然若是開發的是一個基礎類庫,或者基礎平臺,仍是直接使用ASM吧,相信從事這方面工做的開發者能力應該比較高。

上一張國外博客的圖,展現處理Java字節碼的工具的關係。 接下來介紹如何使用Javassit來操做字節碼

Javassit使用方法

Javassist是一個開源的分析、編輯和建立Java字節碼的類庫。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba (千葉 滋)所建立的。它已加入了開放源代碼JBoss 應用服務器項目,經過使用Javassist對字節碼操做爲JBoss實現動態AOP框架。javassist是jboss的一個子項目,其主要的優勢,在於簡單,並且快速。直接使用java編碼的形式,而不須要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。

Javassist中最爲重要的是ClassPoolCtClassCtMethod 以及 CtField這幾個類。

ClassPool:一個基於HashMap實現的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認的ClassPool使用與底層JVM相同的類路徑,所以在某些狀況下,可能須要向ClassPool添加類路徑或類字節。

CtClass:表示一個類,這些CtClass對象能夠從ClassPool得到。

CtMethods:表示類中的方法。

CtFields :表示類中的字段。

動態生成一個類

下面的代碼會生成一個實現了Cloneable接口的類GenerateClass

public void DynGenerateClass() {
     ClassPool pool = ClassPool.getDefault();
     CtClass ct = pool.makeClass("top.ss007.GenerateClass");//建立類
     ct.setInterfaces(new CtClass[]{pool.makeInterface("java.lang.Cloneable")});//讓類實現Cloneable接口
     try {
         CtField f= new CtField(CtClass.intType,"id",ct);//得到一個類型爲int,名稱爲id的字段
         f.setModifiers(AccessFlag.PUBLIC);//將字段設置爲public
         ct.addField(f);//將字段設置到類上
         //添加構造函數
         CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct);
         ct.addConstructor(constructor);
         //添加方法
         CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct);
         ct.addMethod(helloM);

         ct.writeFile();//將生成的.class文件保存到磁盤

         //下面的代碼爲驗證代碼
         Field[] fields = ct.toClass().getFields();
         System.out.println("屬性名稱:" + fields[0].getName() + "  屬性類型:" + fields[0].getType());
     } catch (CannotCompileException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     } catch (NotFoundException e) {
         e.printStackTrace();
     }
 }

上面的代碼就會動態生成一個.class文件,咱們使用反編譯工具,例如Bytecode Viewer,查看生成的字節碼文件GenerateClass.class,以下圖所示。

動態添加構造函數及方法

有不少種方法添加構造函數,咱們使用CtNewConstructor.make,他是一個的靜態方法,其中有一個重載版本比較方便,以下所示。第一個參數是source text 類型的方法體,第二個爲類對象。

CtConstructor constructor=CtNewConstructor.make("public GeneratedClass(int pId){this.id=pId;}",ct); ct.addConstructor(constructor);

這段代碼執行後會生成以下java代碼,代碼片斷是使用反編譯工具JD-GUI產生的,能夠看到構造函數的參數名被修改爲了paramInt

public GeneratedClass(int paramInt) { 
     this.id = paramInt; 
 }

一樣有不少種方法添加函數,咱們使用CtNewMethod.make這個比較簡單的形式

CtMethod helloM=CtNewMethod.make("public void hello(String des){ System.out.println(des);}",ct); ct.addMethod(helloM);

這段代碼執行後會生成以下java代碼:

public void hello(String paramString) {
    System.out.println(paramString); 
}

動態修改方法體

動態的修改一個方法的內容纔是咱們關注的重點,例如在AOP編程方面,咱們就會用到這種技術,動態的在一個方法中插入代碼。 例如咱們有下面這樣一個類

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

咱們要動態的在內存中在move()方法體的先後插入一些代碼

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();
            //經過反射調用方法,查看結果
            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);
        }
        ...
    }

使用反編譯工具查看修改後的move方法結果:

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;//方法返回值
    System.out.println(this.x);System.out.println(this.y);
  }

能夠看到,在生成的字節碼文件中確實增長了相應的代碼。 函數輸出結果爲:

dx:1dy:2 
2 
4

Javassit 還有許多功能,例如在方法中調用方法,異常捕捉,類型強制轉換,註解相關操做等,並且其還提供了字節碼層面的API(Bytecode level API)。

本文轉載於http://www.javashuo.com/article/p-vhlzqlyu-nm.html

對轉載原文的補充

若是想對類進行修改,最好是在該類在被類加載器以前。否則在執行 CtClass.toCalss() 或者 CtClass.toBytese ,會出現duplicate class definition 的異常。固然也可使用 Javassit 提供的類加載器 Loader ,解決被同一個類加載器加載衝突的問題。可是須要注意的是,不一樣類加載器加載的同一個類,不是相同的類。

關於這一點能夠參考 https://blog.csdn.net/qq_26222859/article/details/52600260

相關文章
相關標籤/搜索