使用CodeModel生成Java類

為什麼不問問神奇的滑稽呢發表於2017-12-21

CodeModel

CodeModel是什麼

CodeModel是一個Java庫,使用它能通過Java程式碼生成Java類。乍看之下,除了“不明覺厲”,想不出來還能用什麼詞來形容他,說得不好聽一點,有什麼用呢?

怎麼用

說完了是什麼,接下來就是怎麼用。本篇文章只需要講解基本用法,所以IDE就使用Intellij Idea 2017 社群版。 為了方便引用庫,所以新建一個Gradle的Java專案,然後在專案的build.gradle檔案中新增依賴:

compile 'com.sun.codemodel:codemodel:2.6'
複製程式碼

操作方法跟Android Studio裡的差不多。

開始吧

如何生成一個類

很簡單,看程式碼:

public class Main {
    public static void main(String[] args) throws JClassAlreadyExistsException, IOException {
        //例項化JCodeModel物件
        JCodeModel jCodeModel = new JCodeModel();
        //生成package
        JPackage jPackage = jCodeModel._package("com.peceoqicka.cm");
        //在指定package下生成類
        jPackage._class("MyClass");
        //將生成的類程式碼寫入檔案
        jCodeModel.build(new File("src/main/java/"));
    }
}
複製程式碼

API中常用的類

類名 說明
JPackage 包型別,可由JCodeModel#_package方法得到,主要作用是呼叫_class方法在指定包裡生成類
JDefinedClass 定義類型別,提供各種方法如向定義類裡新增方法或者成員等
JClass 已有類型別,通常為引用已經有的類如String
JFieldVar 成員變數型別
JMethod 方法型別
JBlock 程式碼塊型別,通常由JMethod#_body方法獲得,用於向方法中新增程式碼
JArray Java陣列型別
JExpression Java表示式型別
JExpr Java表示式工具類,用於生成JExpression
Jvar 區域性變數型別

成員變數與區域性變數

成員變數

成員變數的定義:

JFieldVar fieldVar = jDefinedClass.field(JMod.PRIVATE+JMod.STATIC+JMod.FINAL,
			String.class, "CONSTANT_STR_NAME", JExp.lit("Gradle"));
複製程式碼

程式碼執行的結果是:

public class MyClass {
    public final static String CONSTANT_STR_NAME = "Gradle";
}
複製程式碼

很好理解,一共4個引數,第一個引數定義變數的修飾符,凡是你能想到的例如public、static、private等等都是引用的JMod這個類裡邊的常量;第二個引數定義變數的型別,可以直接傳入已有的型別的class,或者引用用JCodeModel生成的型別(JType);第三個引數定義變數的名稱;第四個引數是可選的,如果你需要在定義的時候初始化這個變數,那就傳入第四個引數,否則就不傳。

field方法一共有4個重構方法:

field(int mods, Class<?> type, String name)
field(int mods, JType type, String name)
field(int mods, Class<?> type, String name, JExpression init)
field(int mods, JType type, String name, JExpression init)
複製程式碼

其中JExpression是表示式類,可以通過JExpr工具類來獲得,直接賦值可以使用:

JExpr.lit(String n)
JExpr.lit(int n)
...
複製程式碼

String以及所有基本值型別都可以通過這種方法來賦值。通過等號賦值請參看方法部分的說明。

區域性變數

區域性變數的定義:

JVar jVar = methodBody.decl(jCodeModel.INT, "i");
methodBody.assign(jVar, JExpr.lit(6));
複製程式碼

程式碼執行的結果:

int i;
i = 6;
複製程式碼

decl方法的重構方法:

decl(JType type, String name)
decl(JType type, String name, JExpression init)
decl(int mods, JType type, String name, JExpression init)//用於定義final區域性變數
複製程式碼

那麼如果區域性變數的賦值是一個方法呼叫的結果怎麼辦,別急:

methodBody.assign(jVar, JExpr.invoke(jVarClass, "methodAnother")
     .arg(JExpr.lit(5));
複製程式碼

這行程式碼對應的生成程式碼為:

i = someClass.methodAnother(5);
複製程式碼

可以很明顯的看出來,方法呼叫用JExpression來表達就是JExpr.invoke,第一個引數是指明呼叫的方法是屬於哪個類的例項的,第二個引數指明方法名稱。如果呼叫的方法要傳入引數,那麼直接在invoke後方呼叫arg方法傳入,有多少個引數就呼叫多少次arg方法,都是鏈式呼叫。

方法

方法的定義:

JMethod jMethodGWC = jDefinedClass.method(JMod.PUBLIC, jCodeModel.INT, "getWheelCount");
JBlock jBlockGWC = jMethodGWC.body();
JVar jVarC = jBlockGWC.decl(jCodeModel.INT, "count");
jBlockGWC.assign(jVarC, JExpr.lit(6));
jBlockGWC._return(jVarC);
複製程式碼

對應的程式碼:

public int getWheelCount() {
    int count;
    count = 6;
    return count;
}
複製程式碼

為方法新增引數:

jMethod.param(jCodeModel.INT, "type");//int type
複製程式碼

傳入引數與定義區域性變數的decl方法的一致。JBlock的_return方法定義了方法體的返回語句。

建構函式

建構函式就是沒有返回值的方法,定義:

JMethod constructor = jDefinedClass.constructor(JMod.PUBLIC);
//public Car(){}
複製程式碼

那麼新增引數和在方法體中新增程式碼的方法和普通方法完全一樣。

類和介面

類的定義:

JDefinedClass jDefinedClass = jPackage._class("Car");
//public class Car{}
複製程式碼

預設為public,定義抽象類直接在引數裡新增JMod.ABSTRACT。

JDefinedClass jDefinedClass = jPackage._class(JMod.ABSTRACT, "Car");
//public abstract class Car{}
複製程式碼

定義介面:

JDefinedClass jDefinedClass = jPackage._class(JMod.ABSTRACT, "ICar", ClassType.INTERFACE);
//public interface ICar{}
複製程式碼

繼承和實現(介面):

jDefinedClass._extends(MyClass.class);
jDefinedClass._implements(MyInterface.class);
複製程式碼

引用已有的型別:

JClass jClass = jCodeModel.ref(String.class);
jClass = jCodeModel.ref("java.lang.String");
複製程式碼

條件語句

if...else

定義if條件語句:

JConditional jConditionalGGN = jBlockGGN._if(JExpr.ref("sex").eq(JExpr.lit(1)));
jConditionalGGN._then().block()._return(JExpr.lit("Female"));
jConditionalGGN._else().block()._return(JExpr.lit("Male"));
複製程式碼

對應的程式碼:

if (sex == 1) {
    return "Female";
} else {
    return "Male";
}
複製程式碼

block方法返回的同樣是JBlock型別。

switch

定義switch語句:

JSwitch jSwitchGGNS = jBlockGGNS._switch(JExpr.ref("sex"));
jSwitchGGNS._case(JExpr.lit(1)).body()._return(JExpr.lit("Female"));
jSwitchGGNS._case(JExpr.lit(0));
jSwitchGGNS._default().body()._return(JExpr.lit("Male"));
複製程式碼

對應的程式碼:

public static String getGenderName(int sex) {
    switch (sex) {
        case 1:
            return "Female";
        case 0:
        default:
            return "Male";
    }
}
複製程式碼

迴圈語句

for迴圈

定義for迴圈:

ForLoop jForLoop = jBlockDS._for();
jForLoop.init(jCodeModel.INT, "i", JExpr.lit(0));//初始化部分
jForLoop.test(JExpr.ref("i").lt(JExpr.lit(100)));//條件判斷部分
jForLoop.update(JExpr.ref("i").incr());//條件變化部分

JBlock jBlockFLDS = jForLoop.body();//獲得for迴圈程式碼塊
JClass jClassSystem = jCodeModel.ref(System.class);
JFieldRef jFieldRefSOut = jClassSystem.staticRef("out");
//for迴圈程式碼塊中呼叫System.out.println
jBlockFLDS.invoke(jFieldRefSOut, "println").arg("hello");
複製程式碼

對應的程式碼:

for (int i = 0; (i< 100); i ++) {
    System.out.println("hello");
}
複製程式碼

foreach迴圈

定義foreach迴圈:

//定義一個字串陣列,並新增初始化資料
JArray jArray = JExpr.newArray(jCodeModel.ref(String.class));
jArray.add(JExpr.lit("Amy"));
jArray.add(JExpr.lit("Bruce"));
jArray.add(JExpr.lit("Cherry"));
jArray.add(JExpr.lit("Douglas"));
jArray.add(JExpr.lit("Ella"));
jArray.add(JExpr.lit("Fin"));
//定義定義區域性變數引用剛才的陣列
JVar arr = jBlockDS.decl(jCodeModel.ref(String[].class), "arr", jArray);

JForEach jForEach = jBlockDS.forEach(jCodeModel.ref(String.class), "str", arr);
JFieldRef jFieldRefSOut = jCodeModel.ref(System.class).staticRef("out");
jForEach.body().invoke(jFieldRefSOut, "println").arg(jForEach.var());
複製程式碼

對應的程式碼:

String[] arr = new String[] {"Amy", "Bruce", "Cherry", "Douglas", "Ella", "Fin"};
for (String str: arr) {
    System.out.println(str);
}
複製程式碼

while迴圈

定義while迴圈:

//定義區域性變數i=0
JVar jVarI = jBlockDS.decl(jCodeModel.INT, "i", JExpr.lit(0));

//生成while語句,新增條件i<10
JWhileLoop jWhileLoop = jBlockDS._while(JExpr.ref("i").lt(JExpr.lit(10)));

//同上呼叫方法System.out.println,引數為i
JFieldRef jFieldRefSOut = jCodeModel.ref(System.class).staticRef("out");
jWhileLoop.body().invoke(jFieldRefSOut, "println").arg(jVarI);

//附加語句i+=1
jWhileLoop.body().assignPlus(jVarI, JExpr.lit(1));
複製程式碼

對應的程式碼:

int i = 0;
while (i< 10) {
      System.out.println(i);
      i += 1;
}
複製程式碼

do...while迴圈

定義do..while迴圈的程式碼與while的程式碼幾乎完全一致,將第一行改為呼叫_do:

JVar jVarI = jBlockDS.decl(jCodeModel.INT, "i", JExpr.lit(0));

//定義do..while迴圈,同樣傳入條件,其餘程式碼幾乎完全一樣
JDoLoop jDoLoop = jBlockDS._do(JExpr.ref("i").lt(JExpr.lit(10)));

JFieldRef jFieldRefSOut = jCodeModel.ref(System.class).staticRef("out"); jDoLoop.body().invoke(jFieldRefSOut, "println").arg(jVarI);
jDoLoop.body().assignPlus(jVarI, JExpr.lit(1));
複製程式碼

對應的程式碼:

int i = 0;
do {
   System.out.println(i);
   i += 1;
} while (i< 10);
複製程式碼

Emmm...

這篇文章作為CodeModel庫的簡要用法參考手冊,是我在學習了僅有的少量資料的情況下自行總結的。最近發現官方文件的頁面不知什麼原因不再維護了,恐怕以後會刪除這個專案,不過庫本身還能在maven倉庫中找到,還能夠繼續使用,只是參考資料不會再有更新了。當然這個庫設計的思路還是很好的,可以作為一個範本,進而學習它的基本原理,然後自己實現這樣一個庫,豈不美哉。(其實這個庫已經落後了,還是用Freemarker吧各位,這篇文章只是歷史遺留物品而已)


本篇文章僅代表個人觀點,難免會有疏漏或者謬誤之處,歡迎批評指正。

參考資料

  1. 官方API文件(已停止維護)
  2. 中文參考手冊
  3. 英文簡要教程

相關文章