1. 程式人生 > >javapoet-讓你不再書寫無聊的程式碼(一)

javapoet-讓你不再書寫無聊的程式碼(一)

前言

自從從事Android開發以來,一直做的應用層開發,程式碼寫多了,感覺一直在一個瓶頸中,每天寫程式碼無非就是調介面,填資料到頁面,再就是做些簡單的自定義View,寫出產品經理希望的介面,然後就完事,也很少做些介面的調優和優化,一直想學習寫java和android更深入的知識點,提升自己的知識技能….

閒話到此為止,最近突然看到一篇文章,這裡是文章連結:Android 利用 APT 技術在編譯期生成程式碼,看完真是感覺真是專案的好幫手,利用註解和javapoet工具在專案編譯期間動態生成程式碼,減少我們在專案開發中模板化程式碼的編寫,而我們現在很多開源框架如butterknife,內部就是利用類似工具來完成程式碼的動態注入和控制元件繫結。

到此就可以引入該篇文章的正式主題,利用javapoet優雅的生成我們專案的模板化程式碼,擺脫重複程式碼書寫的困擾,減輕程式設計師開發所需要的時間,提高編碼效率。
javapoet是android之神JakeWharton開源的一款快速程式碼生成工具,配合APT簡直就是爽得不行不行的,並且使用其API可以自動生成導包語句。

接下來配合github文件,總結下其使用方法!

HelloWorld Example

作為一個第三方的工具,第一步肯定要新增gradle依賴:

compile ‘com.squareup:javapoet:1.9.0’

然後就是Java程式設計師入門的第一個例子

package com.example.helloworld;

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

想必只要是個程式設計師都寫過這樣的例子,但是我們用javapoet生成是這樣的:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC
) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out);

這段程式碼執行的結果就是上面java helloworld方法,在方法裡面我們用MethodSpec定義了一個main函式,然後用addModifiers給程式碼添加了public和static修飾符,returns新增返回值,addParameter新增引數,addStatement給方法新增程式碼塊,可能addStatement裡面的內容有點難以理解,不過只要把$S$T當成佔位符,整個結構想象成String.format()就好理解了。定義好方法之後我們就需要將方法加入類中,利用TypeSpec可以構建相應的類資訊,然後將main方法通過addMethod()新增進去,如此就將類也構建好了。而JavaFile包含一個頂級的Java類檔案,需要將剛剛TypeSpec生成的物件新增進去,通過這個JavaFile我們可以決定將這個java類以文字的形式輸出或者直接輸出到控制檯。

看完第一個例子可能有點感覺,有點意思,以程式碼來生成程式碼,嗯 。

接著看另一個Example

MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();

基本可以猜出生成的程式碼是這樣的,

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}

我們可以看到MethodSpec中有個addCode新方法,接下來就簡單解釋下addCode

  • addCode和addStatement

一般的類名和方法名是可以被模仿書寫的,但是方法中的構造語句是不確定的,這時候就可以用addCode來新增程式碼塊來實現此功能。但是addCode也有其缺點,我們將第一個helloworld用addCode和addStatement生成的程式碼對比看下

//addCode生成的
package com.example.helloworld;

import java.lang.String;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println('Hello World');
  }
}
//addStatement生成的
package com.example.helloworld;

import java.lang.String;
import java.lang.System;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World");
  }
}

可以看出用addStatement生成的多了一行導包語句,也就是說用addStatement生成的程式碼可以自動同時生成導包程式碼語句,同時使用addStatement可以減少手工的分號,換行符,括號書寫,直接使用Javapoet的api時,程式碼的生成簡單得不要不要的。

我麼也可以使用beginControlFlow() + endControlFlow(),替代for迴圈的程式碼拼寫方式,比如這樣:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0")
    .beginControlFlow("for (int i = 0; i < 10; i++)")
    .addStatement("total += i")
    .endControlFlow()
    .build();

我們也可以使用拼接的方式,動態的傳遞迴圈次數控制比如這樣:

.beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)")

但是既然我們使用了javapoet,就可以使用其API來替換這種字元拼接模式,從而維護程式碼的可閱讀性。

javapoet佔位符

佔位符,是一個很重要的概念,無論是javaweb資料庫Dao層資料庫操作的佔位符概念,還是Hibernate中佔位符,都用到了這個概念,現在javapoet任然可以使用,佔位符使一個字串的拼接形式轉化為String.format的格式,提高程式碼的可讀性。

現在來介紹下javapoet中幾個常用的佔位符。

1). $L 文字值

對於字串的拼接,在使用時是很分散的,太多的拼接的操作符,不太容易觀看。為了去解決這個問題,Javapoet提供一個語法,它接收$L去輸出一個文字值,就像String.format(),這個$L可以是字串,基本型別。

比如上面使用字串拼接的改為$L來看下:

MethodSpec main = MethodSpec.methodBuilder("main")
                .returns(int.class)
                .addStatement("int result = 0")
                .beginControlFlow("for (int i = $L; i < $L; i++)", 0, 10)
                .addStatement("result = result $L i", '*')
                .endControlFlow()
                .addStatement("return result")
                .build();

可以看出簡化了不少,我們可以將程式碼中改變的部分通過引數傳入,而對於不變的直接使用javapoet生成。

2). $S 字串

當我們想輸出字串文字時,我們可以使用 $S去輸出一個字串,而如果我們我們使用 $L 並不會幫我們加上雙引號,比如:

public static void test() throws IOException {
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(whatsMyName("slimShady"))
                .addMethod(whatsMyName("eminem"))
                .addMethod(whatsMyName("marshallMathers"))
                .build();

        JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
                .build();

        javaFile.writeTo(System.out);
    }

    private static MethodSpec whatsMyName(String name) {
        return MethodSpec.methodBuilder(name)
                .returns(String.class)
                .addStatement("return $S", name)
                .build();
    }

這段程式碼的輸出是這樣的:

package com.example.helloworld;

import java.lang.String;

public final class HelloWorld {
  String slimShady() {
    return "slimShady";
  }

  String eminem() {
    return "eminem";
  }

  String marshallMathers() {
    return "marshallMathers";
  }
}

而如果使用 $L的輸出是這樣的

...
  String slimShady() {
    return slimShady;
  }

  String eminem() {
    return eminem;
  }

  String marshallMathers() {
    return marshallMathers;
  }
....

這個編譯的時候肯定會報錯的。

3). $T 物件

對於我們Java開發人來說,JDK和SDK提供的各種java和android的API極大程度的幫助我們開發應用程式。對於Javapoet它也充分支援各種Java型別,包括自動生成導包語句,僅僅使用 $T 就可以了。

MethodSpec methodSpec = MethodSpec.methodBuilder("today")
      .returns(Date.class)
      .addStatement("return new $T", Date.class)
      .build();

我們通過傳入 Date.class來生成程式碼,我們也可以直接通過反射獲取一個ClassName物件,然後傳入。

ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");

MethodSpec today = MethodSpec.methodBuilder("tomorrow")
        .returns(hoverboard)
        .addStatement("return new $T()", hoverboard)
        .build();

簡直屌爆了….., 好吧,我們也只能用這輪子了。

靜態匯入

Javpoet也可以支援靜態匯入,它通過顯示地收集型別成員名來實現,讓我們還是看例子吧。

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
       .addStaticImport(hoverboard, "chen")
       .addStaticImport(hoverboard, "xiao")
       .addStaticImport(Collections.class, "*")
       .build();

靜態匯入,我們只需要在JavaFile的builder中鏈式呼叫addStaticImport方法就可以,第一個引數為Classname物件,第二個為需要匯入的物件中的靜態方法。我們應該根據需要匹配和調整所有的呼叫,並匯入所有其他型別。

4).$N 名字

有時候生成的程式碼是我們自己需要引用的,這時候可以使用 $N來呼叫根據生成的方法名。

MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
        .addParameter(int.class, "i")
        .returns(char.class)
        .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
        .build();

MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
        .addParameter(int.class, "b")
        .returns(String.class)
        .addStatement("char[] result = new char[2]")
        .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
        .addStatement("result[1] = $N(b & 0xf)", hexDigit)
        .addStatement("return new String(result)")
        .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addMethod(hexDigit)
        .addMethod(byteToHex)
        .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
        .build();

javaFile.writeTo(System.out);

在上面例子中,byteToHex想要呼叫 hexDigit方法,我們就可以使用 $N來呼叫,hexDigit()方法作為引數傳遞給byteToHex()方法通過使用$N來達到方法自引用。

程式碼塊格式字串

相關引數格式化

CodeBlock codeBlock = CodeBlock.builder().add("I ate $L $L", 3, "ta").build();
System.out.println(codeBlock.toString());

輸出:

I ate 3 ta

我們也可以通過位置引數指定要用的引數的位置:

CodeBlock.builder().add("I ate $2L $1L", "tacos", 3)

在 $的後面指定需要的引數位置序號,非常方便。

名字引數

通過$argumentName:X這樣的語法形式來達到通過key名字尋找值,然後使用的功能,引數名可以使用 a-z, A-Z, 0-9, and _ ,但是必須使用小寫字母開頭。

Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos"); //map的key必須小寫字母開頭
map.put("count", 3);
CodeBlock.builder().addNamed("I ate $count:L $food:L", map)

構造方法

MethodSpec 也可以生成構造方法

MethodSpec flux = MethodSpec.constructorBuilder()
        .addModifiers(Modifier.PUBLIC)
        .addParameter(String.class, "greeting")
        .addStatement("this.$N = $N", "greeting", "greeting")
        .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC)
        .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL)
        .addMethod(flux)
        .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
        .build();

javaFile.writeTo(System.out);

輸出:

package com.example.helloworld;

import java.lang.String;

public class HelloWorld {
  private final String greeting;

  public HelloWorld(String greeting) {
    this.greeting = greeting;
  }
}

還是很好理解的

方法引數

經常的我們需要給方法加入口引數,此時我們就可以通過ParameterSpec來達到這一目的

ParameterSpec android = ParameterSpec.builder(String.class, "android")
    .addModifiers(Modifier.FINAL)
    .build();

MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords")
    .addParameter(android)
    .addParameter(String.class, "robot", Modifier.FINAL)
    .build();

輸出:

void welcomeOverlords(final String android, final String robot) {
}

成員變數

我們可以通過 Fields來達到生成成員變數的作用

FieldSpec android = FieldSpec.builder(String.class, "android")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(android)
    .addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
    .build();

initializer可以初始化成員變數,比如這樣:

.initializer("$S + $L", "Lollipop v.", 5.0d)

各種姿勢,有木有~~~

介面

Javpoet中的介面方法必須始終用PUBLIC ABSTRACT修飾符修飾,而對於欄位Field必須用PUBLIC STATIC FINAL修飾,這些都是非常必要的,當我在生成一個介面時。

TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC)
    .addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
        .initializer("$S", "change")
        .build())
    .addMethod(MethodSpec.methodBuilder("beep")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .build())
    .build();

生成介面物件時,這些修飾符都會省略掉!!!

public interface HelloWorld {
  String ONLY_THING_THAT_IS_CONSTANT = "change";

  void beep();
}

列舉

使用enumBuilder 去建立一個列舉型別,使用addEnumConstant()去新增列舉常量值

TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
    .addModifiers(Modifier.PUBLIC)
    .addEnumConstant("ROCK")
    .addEnumConstant("SCISSORS")
    .addEnumConstant("PAPER")
    .build();

輸出:

public enum Roshambo {
  ROCK,

  SCISSORS,

  PAPER
}

再看一個更加複雜點的例子:

TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
    .addModifiers(Modifier.PUBLIC)
    .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist")
        .addMethod(MethodSpec.methodBuilder("toString")
            .addAnnotation(Override.class)
            .addModifiers(Modifier.PUBLIC)
            .addStatement("return $S", "avalanche!")
            .build())
        .build())
    .addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace")
        .build())
    .addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat")
        .build())
    .addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL)
    .addMethod(MethodSpec.constructorBuilder()
        .addParameter(String.class, "handsign")
        .addStatement("this.$N = $N", "handsign", "handsign")
        .build())
    .build();

輸出:

public enum Roshambo {
  ROCK("fist") {
    @Override
    public void toString() {
      return "avalanche!";
    }
  },

  SCISSORS("peace"),

  PAPER("flat");

  private final String handsign;

  Roshambo(String handsign) {
    this.handsign = handsign;
  }
}

還是很方便的有木有~~~

匿名內部類

對於匿名內部類,我們可以使用Types.anonymousInnerClass()來生成程式碼塊,然後在匿名內部類中使用,可以通過 $L引用

TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
    .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
    .addMethod(MethodSpec.methodBuilder("compare")
        .addAnnotation(Override.class)
        .addModifiers(Modifier.PUBLIC)
        .addParameter(String.class, "a")
        .addParameter(String.class, "b")
        .returns(int.class)
        .addStatement("return $N.length() - $N.length()", "a", "b")
        .build())
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addMethod(MethodSpec.methodBuilder("sortByLength")
        .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
        .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
        .build())
    .build();

輸出:

void sortByLength(List<String> strings) {
  Collections.sort(strings, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
      return a.length() - b.length();
    }
  });
}

註解

註解也是被支援的,這裡有個小例子

MethodSpec toString = MethodSpec.methodBuilder("toString")
    .addAnnotation(Override.class)
    .returns(String.class)
    .addModifiers(Modifier.PUBLIC)
    .addStatement("return $S", "Hoverboard")
    .build();

還是很容易懂的。

最後

這篇文章大部分例子都是github官方介紹的,有不懂的可以直接去官網看。用了一段時間javapoet感覺還是很有用的,最近專案一直寫些重複性很大的程式碼,但是又是兩個人開發的,並且程式碼也很多,所以想改動起來還是很麻煩的,但是還是先了解下,以後說不定哪天會用到的。

本篇文章到這裡基本就結束了,下篇想詳細介紹下註解的使用。

參考