要實現(xiàn)一個功能,我們通常編寫一系列的java文件纳账,如果需求發(fā)生變化幅聘,則修改這些java文件或增加一些新的java文件。為了避免為適應(yīng)千變?nèi)f化的需求而頻繁修改項目代碼余寥,可以在運行時動態(tài)生成字節(jié)碼领铐,當(dāng)然運行時生成字節(jié)碼需要占用計算資源。當(dāng)然宋舷,還有一種思路是根據(jù)條件動態(tài)生成java文件绪撵,而不是根據(jù)每種情況編寫固定的代碼,這樣生成的項目與完全手工編寫的代碼沒有任何區(qū)別祝蝠。JavaPoet就是一個動態(tài)生成java文件的庫音诈,在caffine、butterknife绎狭、自動生成rpc stub文件等中間件中得到了應(yīng)用细溅。
什么是[AOP]
AOP面向切面編程,就是在代碼預(yù)編譯階段儡嘶,在不修改源代碼的情況下喇聊,給程序添加某一功能。
像成熟的框架蹦狂,ARouter誓篱,ButterKnife等也都使用了這個技術(shù)朋贬。任何技術(shù)的出現(xiàn)都有其實際應(yīng)用場景,為了解決某一方面的痛點窜骄。AOP的出現(xiàn)讓某些功能組件的封裝更加解耦锦募,使用者能夠更加的方便的使用組件里的功能。
拿ButterKnife舉例邻遏,我們原生開發(fā)糠亩,以前經(jīng)常寫很多findViewById的代碼,顯然這類代碼寫起來很繁瑣准验,且容易出錯(id和view有時候沒對上)削解。而AOP可以有效避免這些問題。
比如我們可以通過在預(yù)編譯的階段解析注解沟娱,然后生成對應(yīng)的java文件,該java文件封裝了findviewbyid的方法腕柜,實現(xiàn)view和id的動態(tài)綁定济似。這樣就非常有效減少了后期的編寫代碼的工作量,可以快速實現(xiàn)view和id的綁定操作盏缤。
javapoet的運用
JavaPoet是square推出的開源java代碼生成框架砰蠢,提供Java Api生成.java源文件。這個框架功能非常有用唉铜,我們可以很方便的使用它根據(jù)注解台舱、數(shù)據(jù)庫模式、協(xié)議格式等來對應(yīng)生成代碼潭流。通過這種自動化生成代碼的方式竞惋,可以讓我們用更加簡潔優(yōu)雅的方式要替代繁瑣冗雜的重復(fù)工作。引用依賴:
compile 'com.squareup:javapoet:1.7.0'
該項目結(jié)構(gòu)如下:
1 基本功能介紹
我們知道灰嫉,一個java類由類聲明拆宛、字段、構(gòu)造方法讼撒、方法浑厚、參數(shù)、注解等元素組成根盒,JavaPoet為這些基本組成元素分別定義了相應(yīng)的類钳幅,分別用來管理、生成相應(yīng)元素相關(guān)的代碼炎滞。
JavaPoet常用的一些類:
class | 說明 |
---|---|
JavaFile | 對應(yīng)編寫的.java文件 |
TypeSpec | 對應(yīng)一個類敢艰、接口或enum |
MethodSpec | 對應(yīng)一個方法或構(gòu)造方法 |
FieldSpec | 對應(yīng)一個字段 |
ParameterSpec | 對應(yīng)方法或構(gòu)造方法的一個參數(shù) |
AnnotationSpec | 對應(yīng)類型、字段厂榛、方法或構(gòu)造方法上的注解 |
ClassName | 對應(yīng)一個類盖矫、接口或enum的名字丽惭,由package名字和類名字兩部分組成 |
CodeBlock | 代碼塊,一般用來生成{} 包裹起來的數(shù)據(jù)塊 |
ParameterizedTypeName | 泛型中的參數(shù)化類型 |
TypeVariableName | 泛型中的類型變量 |
WildcardTypeName | 泛型中的通配符? |
1.1 一個簡單的例子
public class JavapoetApplication {
public static void main(String[] args) {
try {
// 定義一個方法名為test的方法
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修飾符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值類型
.returns(void.class)
// 方法的參數(shù)
.addParameter(Integer.class, "loop")
// 方法body內(nèi)容
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < loop; i++) {\n"
+ " total += i;\n"
+ "}\n"
+ "System.out.println(\"total value: \" + total);\n")
.build();
// 定義一個類辈双,名字為TestCode
TypeSpec testCode = TypeSpec.classBuilder("TestCode")
// 類修飾符
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
// 添加方法
.addMethod(test)
.build();
// 定義一個java文件责掏,指定package和類定義
JavaFile javaFile = JavaFile.builder("com.javatest.javapoet", testCode)
.build();
// 將java文件內(nèi)容寫入文件中
File file = new File("./javapoet");
javaFile.writeTo(file);
} catch (Exception e) {
//
}
}
public static void test1() throws Exception {
}
}
生成的java文件內(nèi)容為:
package com.javatest.javapoet;
import java.lang.Integer;
public final class TestCode {
public static void test(Integer loop) {
int total = 0;
for (int i = 0; i < loop; i++) {
total += i;
}
System.out.println("total value: " + total);
}
}
1.2 改進(jìn)例子
上面的自動生成代碼中,雖然類和方法聲明湃望、修飾符换衬、返回值類型、包名等是用編程實現(xiàn)的(編程實現(xiàn)就意味這可以參數(shù)化证芭,通過控制參數(shù)生成不同的內(nèi)容)瞳浦,但是方法體的內(nèi)容與手工編寫并沒有什么區(qū)別,像語句分號废士、縮進(jìn)叫潦、換行等都是人工編寫的,看不出自動生成代碼的優(yōu)勢官硝。但JavaPoet提供了addStatement()
矗蕊,beginControlFlow()
,endControlFlow()
氢架,nextControlFlow()
等方法方便代碼生成傻咖。addStatement()
會自動在語句后添加分號,并換行岖研;beginControlFlow()
和nextControlFlow()
會自動添加{
符號并換行卿操,控制后面語句的縮進(jìn);endControlFlow()
會自動添加}
符號孙援,并換行害淤,控制后面語句的縮進(jìn)。
重新編寫上面test方法的生成代碼拓售,生成的結(jié)果代碼是一樣的:
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修飾符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值類型
.returns(void.class)
// 方法的參數(shù)
.addParameter(Integer.class, "loop")
// 方法body內(nèi)容
.addStatement("int total = 0")
.beginControlFlow("for (int i = 0; i < loop; i++)")
.addStatement("total += i")
.endControlFlow()
.addStatement("System.out.println(\"total value: \" + total)")
.build();
1.3 進(jìn)一步改進(jìn)例子
現(xiàn)在生成代碼的格式控制交給javaposet管理了筝家,但是方法體的代碼全是硬編碼的,還沒體現(xiàn)自動生成代碼的靈活性邻辉,JavaPoet提供了L溪王、S、T值骇、N等替換符號來實現(xiàn)這方面的需求莹菱。
進(jìn)一步修改上面test方法的生成代碼,如下:
// 定義一個方法名為test的方法
// 定義一個參數(shù)
ParameterSpec loopParam = ParameterSpec.builder(Integer.class, "loop")
.addModifiers(Modifier.FINAL)
.build();
String total = "total";
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修飾符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值類型
.returns(void.class)
// 方法的參數(shù)
.addParameter(loopParam)
// 方法body內(nèi)容
// $L 會替換為變量total的值
.addStatement("int $L = 0", total)
// $N 會替換為 loopParam的名字
.beginControlFlow("for (int i = 0; i < $N; i++)", loopParam)
.addStatement("$L += i", total)
.endControlFlow()
// $T 會替換為類的名字吱瘩,如果需要道伟,會在文件頭添加相應(yīng)的import語句
// $S 會替換為變量的值,并用""包裹起來
.addStatement("$T.out.println($S + $L)", System.class, "total value: ", total)
.build();
1.4 替換符號功能說明
JavaPoet幾個常用替換符號的功能介紹如下:
替換符號 | 說明 |
---|---|
$T | 參數(shù)是Class對象,替換為Class的名字蜜徽,如果需要祝懂,同時在文件頭添加相應(yīng)的import語句 |
$L | 替換為變量值的字面量值,功能相當(dāng)于字符串的format()方法 |
$S | 也是替換為變量值的字面量值,但是字面量值為被字符串雙引號包裹起來 |
$N | 參數(shù)是ParameterSpec、TypeSpec解虱、MethodSpec等,替換為這些變量的name值 |
替換符號還可以指定替換參數(shù)的位置(Relative Arguments)或參數(shù)的名字(Named Arguments)
例如:
addStatement("System.out.println(\"I ate $L $L\")", 3, "tacos")
和
# 指定參數(shù)位置
addStatement("System.out.println(\"I ate $2L $1L\")", "tacos", 3)
以及
# 指定參數(shù)名字
Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos");
map.put("count", 3);
addStatement("System.out.println(\"I ate $count:L $food:L\")", map)
生成的語句都是
System.out.println("I ate 3 tacos");
2 JavaPoet個組件使用說明
2.1 類型
變量灰蛙、參數(shù)、返回值的類型即可以用java的class來表達(dá)隔躲,javapoet也定義了相應(yīng)的類型表示系統(tǒng)摩梧,對應(yīng)關(guān)系如下:
類別 | 生成的類型舉例 | javapoet表達(dá)方式 | java class表達(dá)方式 |
---|---|---|---|
基本類型 | int | TypeName.INT | int.class |
基本類型包裝類型 | TypeName.BOXED_INT | Integer.class | |
數(shù)組 | int[] | ArrayTypeName.of(int.class) | int[].class |
自定義類型 | TestCode.class | ||
參數(shù)化類型 | List<String> | ParameterizedTypeName.get(List.class, String.class) | |
類型變量 | T | TypeVariableName.get("T") | |
通配符類型 | ? extends String | WildcardTypeName.subtypeOf(String.class) |
2.2 field
字段的生成比較簡單,只要指定字段的修飾符宣旱、類型仅父、名字創(chuàng)建FieldSpec,然后添加到TypeSpec中就可以了
// 顯式創(chuàng)建FieldSpec
FieldSpec android = FieldSpec.builder(String.class, "description")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
// 指定初始化值浑吟,可選
.initializer("$S", "this is a example")
.build();
ParameterizedTypeName type = ParameterizedTypeName.get(List.class, String.class);
FieldSpec android = FieldSpec.builder(type, "name").build();
分別生成:
private final String description = "this is a example";
List<String> name;
2.3 class
TypeSpec.classBuilder("Clazz")
// 抽象類
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
// 泛型
.addTypeVariable(TypeVariableName.get("T"))
// 繼承與接口
.superclass(String.class)
.addSuperinterface(Serializable.class)
.addSuperinterface(ParameterizedTypeName.get(Comparable.class, String.class))
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(Map.class),
TypeVariableName.get("T"),
WildcardTypeName.subtypeOf(String.class)))
// 初始化塊
.addStaticBlock(CodeBlock.builder().build())
.addInitializerBlock(CodeBlock.builder().build())
// 添加字段
// .addField(fieldSpec)
// 構(gòu)造方法和方法
// .addMethod(constructorSpec)
// .addMethod(methodSpec)
// 內(nèi)部類
.addType(TypeSpec.classBuilder("InnerClass").build())
.build();
2.2 interface
通過TypeSpec的interfaceBuilder()
方法創(chuàng)建interface驾霜,其他元素添加跟class差不多
TypeSpec helloWorld = TypeSpec.interfaceBuilder("TestInterface").build();
2.3 enum
通過TypeSpec的enumBuilder()
方法創(chuàng)建interface, 通過addEnumConstant()
添加enum成員
一個基本例子:
TypeSpec helloWorld = TypeSpec.enumBuilder("TestEnum")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("EXAM_0")
.addEnumConstant("EXAM_1")
.addEnumConstant("EXAM_2")
.build();
生成的代碼如下:
public enum TestEnum {
EXAM_0,
EXAM_1,
EXAM_2
}
一個復(fù)雜一點的例子:
TypeSpec testEnum = TypeSpec.enumBuilder("TestEnum")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("EXAM_0", TypeSpec.anonymousClassBuilder("$S", "exam0")
.addMethod(MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "exam0")
.returns(String.class)
.build())
.build())
.addEnumConstant("EXAM_1", TypeSpec.anonymousClassBuilder("$S", "exam1")
.build())
.addEnumConstant("EXAM_2", TypeSpec.anonymousClassBuilder("$S", "exam2")
.build())
.addField(String.class, "name", Modifier.PRIVATE, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addParameter(String.class, "name")
.addStatement("this.$N = $N", "name", "name")
.build())
.build();
生成如下代碼:
public enum TestEnum {
EXAM_0("exam0") {
@Override
public String toString() {
return "exam0";
}
},
EXAM_1("exam1"),
EXAM_2("exam2");
private final String name;
Roshambo(String name) {
this.name = name;
}
}
2.4 匿名內(nèi)部類
使用TypeSpec的anonymousClassBuilder()
方法生成匿名內(nèi)部類,舉例如下:
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 testCode = TypeSpec.classBuilder("TestCode")
.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();
}
});
}
2.5 annotation
給方法增加基本注解:
MethodSpec toString = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.returns(String.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "testCode")
.build();
給方法增加帶參數(shù)值的注解:
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(AnnotationSpec.builder(Headers.class)
.addMember("accept", "$S", "application/json; charset=utf-8")
.addMember("userAgent", "$S", "Square Cash")
.build())
.addParameter(LogRecord.class, "logRecord")
.returns(LogReceipt.class)
.build();
2.6 javadoc
類和方法都可以通過TypeSpec和MethodSpec的addJavadoc()
方法添加javadoc
MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
.addJavadoc("Hides {@code message} from the caller's history. Other\n"
+ "participants in the conversation will continue to see the\n"
+ "message in their own history unless they also delete it.\n")
.addJavadoc("\n")
.addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n"
+ "conversation for all participants.\n", Conversation.class)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addParameter(Message.class, "message")
.build();
生成的結(jié)果為:
/**
* Hides {@code message} from the caller's history. Other
* participants in the conversation will continue to see the
* message in their own history unless they also delete it.
*
* <p>Use {@link #delete(Conversation)} to delete the entire
* conversation for all participants.
*/
void dismiss(Message message);
2.7 import 靜態(tài)字段或方法
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards");
JavaFile.builder("com.example.helloworld", hello)
.addStaticImport(hoverboard, "createNimbus")
.addStaticImport(namedBoards, "*")
.addStaticImport(Collections.class, "*")
.build();
生成代碼如下:
package com.example.helloworld;
import static com.mattel.Hoverboard.Boards.*;
import static com.mattel.Hoverboard.createNimbus;
import static java.util.Collections.*;
class HelloWorld {
}
AutoService注解無法生成META-INF文件?
在寫注解處理器時买置,首先就是要繼承AbstractProcessor,并且按照如下步驟聲明:
- 需要在 processors 庫的 main 目錄下新建 resources 資源文件夾强霎;
- 在 resources文件夾下建立 META-INF/services 目錄文件夾忿项;
- 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
- 在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱城舞,包括包路徑轩触;
這樣聲明下來也太麻煩了?這就是用引入auto-service的原因家夺。
在類的頂部加入注解:@AutoService(Processor.class)脱柱,這個注解處理器是Google開發(fā)的,可以用來生成 META-INF/services/javax.annotation.processing.Processor 文件信息拉馋。
使用遇到的問題
在module_processor中導(dǎo)入我們要用的auto-service庫;
implementation 'com.google.auto.service:auto-service:1.0-rc6'
在類上面添加service的注解即可:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...}
編譯項目后卻始終不見META-INF目錄的生成榨为,正常是會在該注解處理器項目的目錄module_processor/build/classes/java/main/META-INF下生成。
注意:可能是版本兼容問題煌茴,把版本降低一點
我們用到了AutoService, 使用@AutoService(Processor.class)随闺,編譯后
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.[STATIC](https://so.csdn.net/so/search?q=STATIC&spm=1001.2101.3001.7020))
.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, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
AutoService會自動在META-INF文件夾下生成Processor配置信息文件,該文件里就是實現(xiàn)該服務(wù)接口的具體實現(xiàn)類蔓腐。而當(dāng)外部程序裝配這個模塊的時候矩乐,
就能通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名,并裝載實例化,完成模塊的注入散罕。