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感覺還是很有用的,最近專案一直寫些重複性很大的程式碼,但是又是兩個人開發的,並且程式碼也很多,所以想改動起來還是很麻煩的,但是還是先了解下,以後說不定哪天會用到的。
本篇文章到這裡基本就結束了,下篇想詳細介紹下註解的使用。