JavaPoet - 優(yōu)雅地生成代碼
[TOC]
一、項目簡介
JavaPoet是square推出的開源java代碼生成框架,提供Java Api生成.java源文件。這個框架功能非常有用,我們可以很方便的使用它根據(jù)注解、數(shù)據(jù)庫模式、協(xié)議格式等來對應生成代碼。通過這種自動化生成代碼的方式,可以讓我們用更加簡潔優(yōu)雅的方式要替代繁瑣冗雜的重復工作。
項目主頁及源碼:https://github.com/square/javapoet
二、項目總覽
該項目代碼量相對較小,只有一個package(com.squareup.javapoet),所有類均位于該package下。
2.1 大體結(jié)構(gòu)圖

2.2 關(guān)鍵類說明
| class | 說明 | |
|---|---|---|
| JavaFile | A Java file containing a single top level class | 用于構(gòu)造輸出包含一個頂級類的Java文件 |
| TypeSpec | A generated class, interface, or enum declaration | 生成類,接口,或者枚舉 |
| MethodSpec | A generated constructor or method declaration | 生成構(gòu)造函數(shù)或方法 |
| FieldSpec | A generated field declaration | 生成成員變量或字段 |
| ParameterSpec | A generated parameter declaration | 用來創(chuàng)建參數(shù) |
| AnnotationSpec | A generated annotation on a declaration | 用來創(chuàng)建注解 |
在JavaPoet中,JavaFile是對.java文件的抽象,TypeSpec是類/接口/枚舉的抽象,MethodSpec是方法/構(gòu)造函數(shù)的抽象,F(xiàn)ieldSpec是成員變量/字段的抽象。這幾個類各司其職,但都有共同的特點,提供內(nèi)部Builder供外部更多更好地進行一些參數(shù)的設(shè)置以便有層次的擴展性的構(gòu)造對應的內(nèi)容。
另外,它提供$L(for Literals), $S(for Strings), $T(for Types), $N(for Names)等標識符,用于占位替換。
三、相關(guān)使用
3.1 API使用
關(guān)于JavaPoet 的API使用,官方Github主頁已經(jīng)有很詳細的使用說明和示例了,具體可前往查看。此處不贅述,詳見 項目主頁、源碼及使用說明
3.2 一個簡單示例
下面就讓我們以一個簡單HelloWorld的例子來開啟我們的JavaPoet之旅。
引入庫:
build.gradle
compile 'com.squareup:javapoet:1.9.0'
例子如下:
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
上方的代碼是通過下方代碼調(diào)用JavaPoet的API生成的:
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, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
四、源碼淺析
下面來看看調(diào)用了JavaFile的writeTo后實際做了些什么。
public void writeTo(Appendable out) throws IOException {
// First pass: emit the entire class, just to collect the types we'll need to import.
CodeWriter importsCollector = new CodeWriter(NULL_APPENDABLE, indent, staticImports);
emit(importsCollector);
Map<String, ClassName> suggestedImports = importsCollector.suggestedImports();
// Second pass: write the code, taking advantage of the imports.
CodeWriter codeWriter = new CodeWriter(out, indent, suggestedImports, staticImports);
emit(codeWriter);
}
通過源碼可以知道,writeTo分為兩部分:第一步收集import,記錄下來后第二步才跟隨內(nèi)容一起寫到CodeWriter。
另外我們可以看到源碼中的emit方法,通過查看其它源碼發(fā)現(xiàn),在JavaPoet中,所有java文件的抽象元素都定義了emit方法,如TypeSepc,ParameterSepc等,emit方法傳入CodeWriter對象輸出字符串。上層元素調(diào)用下層元素的emit方法,如JavaFile的emit方法調(diào)用TypeSpec的emit方法,從而實現(xiàn)整個java文件字符串的生成。
下面我們以MethodSpec為例,查看其emit代碼:
void emit(CodeWriter codeWriter, String enclosingName, Set<Modifier> implicitModifiers)
throws IOException {
codeWriter.emitJavadoc(javadoc);
codeWriter.emitAnnotations(annotations, false);
codeWriter.emitModifiers(modifiers, implicitModifiers);
if (!typeVariables.isEmpty()) {
codeWriter.emitTypeVariables(typeVariables);
codeWriter.emit(" ");
}
if (isConstructor()) {
codeWriter.emit("$L(", enclosingName);
} else {
codeWriter.emit("$T $L(", returnType, name);
}
boolean firstParameter = true;
for (Iterator<ParameterSpec> i = parameters.iterator(); i.hasNext(); ) {
ParameterSpec parameter = i.next();
if (!firstParameter) codeWriter.emit(",").emitWrappingSpace();
parameter.emit(codeWriter, !i.hasNext() && varargs);
firstParameter = false;
}
codeWriter.emit(")");
if (defaultValue != null && !defaultValue.isEmpty()) {
codeWriter.emit(" default ");
codeWriter.emit(defaultValue);
}
if (!exceptions.isEmpty()) {
codeWriter.emitWrappingSpace().emit("throws");
boolean firstException = true;
for (TypeName exception : exceptions) {
if (!firstException) codeWriter.emit(",");
codeWriter.emitWrappingSpace().emit("$T", exception);
firstException = false;
}
}
if (hasModifier(Modifier.ABSTRACT)) {
codeWriter.emit(";\n");
} else if (hasModifier(Modifier.NATIVE)) {
// Code is allowed to support stuff like GWT JSNI.
codeWriter.emit(code);
codeWriter.emit(";\n");
} else {
codeWriter.emit(" {\n");
codeWriter.indent();
codeWriter.emit(code);
codeWriter.unindent();
codeWriter.emit("}\n");
}
}
可以看出,MethodSepc通過調(diào)用codeWriter的emit方法依次輸出javadoc,annotation,parameter,codeblock等。
五、使用場景
5.1 根據(jù)編譯時注解生成代碼
5.1.1 前言
用過butterknife的同學會發(fā)現(xiàn),使用butterknife我們可以省去平時重復書寫的findViewById之類的代碼,通過注解的方式即可實現(xiàn)。而早期的butterknife使用的注解是運行時注解,即運行時通過注解然后使用反射實現(xiàn),存在一定的性能問題,后面作者做了改進,使用編譯時注解,編譯期間,在注解處理器中對注解進行處理生成相應代碼。
通過查看butterknife源碼,如下:
- build.gradle (butterknife-parent)
ext.deps = [
...
javapoet: 'com.squareup:javapoet:1.8.0',
...
]
- build.gradle (butterknife-compiler)
dependencies {
...
compile deps.javapoet
...
}
- ButterKnifeProcessor.java (butterknife-compiler)
(注解處理器)
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
可以看到butterknife在編譯時在Processor中獲取對應的注解,然后使用JavaPoet進行代碼生成工作。(事實上開源框架Dagger也使用了JavaPoet)
5.1.2 一個簡單示例
本節(jié)將簡單演示利用編譯時注解+JavaPoet來實現(xiàn)編譯期間動態(tài)生成代碼。
工程目錄結(jié)構(gòu):
- Hello
- app
- hello-annotation (注解相關(guān))
- hello-compiler (處理器生成代碼相關(guān))
①. 導入依賴:
build.gralde (project)
buildscript {
...
dependencies {
...
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
build.gradle (Module:app)
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
...
compile project(':hello-annotation')
apt project(':hello-compiler')
}
build.gradle (Module:hello-compiler)
dependencies {
...
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
}
注: 自Android Gradle 插件 2.2 版本開始,官方提供了名為 annotationProcessor 的功能來完全代替 android-apt。
若工程使用gradle版本>=2.2,則此處無需引用com.neenbedankt.android-apt相關(guān),將 apt project(':hello-compiler') 改為 annotationProcessor project(':hello-compiler') 即可。
②. 定義注解: (Module:hello-annotation)
HelloAnnotation.java
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface HelloAnnotation {
}
③. 定義Processor: (Module:hello-compiler)
HelloProcessor.java
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler(); // for creating file
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement element : annotations) {
if (element.getQualifiedName().toString().equals(HelloAnnotation.class.getCanonicalName())) {
// main method
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();
// HelloWorld class
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// build com.example.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.example", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// write to file
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(HelloAnnotation.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
④. 使用注解并調(diào)用生成的類函數(shù)
MainActivity.java (Module:app)
@HelloAnnotation
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HelloWorld.main(null);
}
}
未編譯前,HelloWorld.java是不存在的,這里會報錯。那么,我們嘗試編譯一下,就會發(fā)現(xiàn)HelloWorld.java會自動生成,如下:
// This codes are generated automatically. Do not modify!
package com.example;
import java.lang.String;
import java.lang.System;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
5.2 根據(jù)協(xié)議文件生成對應代碼
假設(shè)我們對類的聲明以及接口的聲明是以特定格式寫在一個協(xié)議文件中,那么我們可以先讀取該協(xié)議文件內(nèi)容,使用JavaPoet根據(jù)協(xié)議對應生成Java代碼。
如定義以下協(xié)議文件:
service TestDemo {
rpc doRequest (MyRequest) returns (MyResponse) { // 請求接口定義
}
message MyRequest { // 請求內(nèi)容實體
string content;
}
message MyResponse { // 返回內(nèi)容實體
int32 status_code;
string entity;
}
}
那么利用JavaPoet我們可以生成對應的TestDemo.java, MyRequest.java, MyResponse.java, 以及TestDemo.java中對應的請求接口和實現(xiàn)。
注:此部分協(xié)議定義參考自google開源的protobuffer和grpc
5.3 更多待擴展
六、知識儲備
6.1 注解處理器(Annotation Processor)
注解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)。你可以自定義注解,并注冊相應的注解處理器(自定義的注解處理器需繼承自AbstractProcessor)。
6.1.1 自定義注解處理器
定義一個注解處理器,需要繼承自AbstractProcessor。如下所示:
package com.example;
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
- init(ProcessingEnvironment env): 每一個注解處理器類都必須有一個空的構(gòu)造函數(shù)。然而,這里有一個特殊的init()方法,它會被注解處理工具調(diào)用,并輸入ProcessingEnviroment參數(shù)。ProcessingEnviroment提供很多有用的工具類如Elements, Types和Filer等。
- process(Set<? extends TypeElement> annotations, RoundEnvironment env): 這相當于每個處理器的主函數(shù)main()。你在這里寫你的掃描、評估和處理注解的代碼,以及生成Java文件。輸入?yún)?shù)RoundEnviroment,可以讓你查詢出包含特定注解的被注解元素。
- getSupportedAnnotationTypes(): 這里你必須指定,這個注解處理器是注冊給哪個注解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的注解類型的合法全稱。
- getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這里返回SourceVersion.latestSupported()。
注: 注解處理器是運行在獨立的虛擬機JVM中,javac啟動一個完整Java虛擬機來運行注解處理器。
6.1.2 注冊注解處理器
那么,如何將我們自定義的處理器MyProcessor注冊到javac中呢?首先我們需要將我們的注解處理器打包到一個jar文件中,其次在這個jar中,需要打包一個特定的文件javax.annotation.processing.Processor到META-INF/services路徑下。以下是這個jar的大致結(jié)構(gòu)示意圖:
- MyProcessor.jar
- com
- example
- MyProcessor.jar
- example
- META-INF
- services
- javax.annotation.processing.Processor
- services
- com
打包進MyProcessor.jar中的javax.annotation.processing.Processor的內(nèi)容是,注解處理器的合法的全名列表,每一個元素換行分割:
com.example.MyProcessor
com.foo.OtherProcessor
net.blabla.SpecialProcessor
把MyProcessor.jar放到你的builpath中,javac會自動檢查和讀取javax.annotation.processing.Processor中的內(nèi)容,并且注冊MyProcessor作為注解處理器。
6.1.3 com.google.auto.service:auto-service
Google提供了一個插件來幫助我們更方便的注冊注解處理器,你只需要導入對應的依賴包,在自定義的Processor類上方添加@AutoService(Processor.class)即可。如下:
- 導入依賴包
compile 'com.google.auto.service:auto-service:1.0-rc2'
- 添加聲明
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
...
}
6.1.4 com.neenbedankt.android-apt
該插件用于處理注解處理器,用法如下:
- 添加plugin聲明:
apply plugin: 'com.neenbedankt.android-apt'
- 添加classpath聲明:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
- 添加處理聲明:
apt project(':xxx-compiler')
注: 自Android Gradle 插件 2.2 版本開始,官方提供了名為 annotationProcessor 的功能來完全代替 android-apt。
若工程使用gradle版本>=2.2,則無需引用com.neenbedankt.android-apt相關(guān),將原先的 apt project(':xxx-compiler') 改為 annotationProcessor project(':xxx-compiler') 即可。
七、小結(jié)
- JavaPoet為square出品,并且諸如butterknife、Dagger等著名開源框架也使用該庫,可見其質(zhì)量保障性和穩(wěn)定性。
- JavaPoet提供的api清晰明了,使用起來簡單方便,功能方面也很齊全,發(fā)布了很久目前也已迭代了很多個版本,趨于穩(wěn)定階段。
- 運用JavaPoet預生成代碼的方式,在省去我們頻繁書寫重復代碼的同時,也避免了使用運行時反射造成的效率問題。