javapoet基本用法翻譯

javapoet 在註解處理器中使用#View on GitHub

‘JavaPoet’ 是一個使用Java API生成 ‘.java’ 源文件的庫,在使用註解處理器或者操作元數據文件時(數據庫模式,協議格式等),動態生成源文件非常有用。通過生成代碼,你不用寫模板代碼也能保證元數據的唯一來源。

例如:

這是個 HelloWorld 類:

package com.example.helloworld;

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

這是用JavaPoet生成 HelloWorld 類的代碼:

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);

首先創建 MethodSpec, 定義main方法,並設置修飾符,返回類型,參數,方法體,然後把 main 方法添加到 HelloWorld 類裏並寫入到 HelloWorld.java 文件。

在這個例子裏,我們把文件寫到System.out,但是我們也能獲取文件內容(JavaFile.toString())或者把文件寫入到文件系統(JavaFile.writeTo())。

Javadoc 記錄了完整的 JavaPoet API,這些API我們接下來進行探討。

編碼 & 控制流程

大多數JavaPoet API只是簡單創建臨時Java對象,而且使用了builder模式,方法鏈式調用和可變參數等便捷編碼方式。
JavaPoet 用 TypeSpec 表示類和接口, FieldSpec 表示字段, MethodSpec 表示方法(注:構造方法也是方法), ParameterSpec 表示參數, AnnotationSpec 表示註解。

使用字串來表示代碼塊,表示方法體和函數體。

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;
  }
}

手工輸入分號,換行和縮進非常繁瑣,所以JavaPoet API提供了更簡單的方式。addStatement()提供分號和換行,beginControlFlow() + endControlFlow()成對使用提供開始結束括弧,換行和縮進。

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

這個例子沒有什麼用,因爲for循環裏都是常量寫死了範圍。請看下面的例子,循環範圍可變且可操作:

private MethodSpec computeRange(String name, int from, int to, String op) {
  return MethodSpec.methodBuilder(name)
      .returns(int.class)
      .addStatement("int result = 1")
      .beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)")
      .addStatement("result = result " + op + " i")
      .endControlFlow()
      .addStatement("return result")
      .build();
}

調用以上JavaPoet方法computeRange("multiply10to20", 10, 20, "*"),生成如下代碼:

int multiply10to20() {
  int result = 1;
  for (int i = 10; i < 20; i++) {
    result = result * i;
  }
  return result;
}

JavaPoet生成可閱讀的源代碼而非字節碼,所以你可以校對是否正確。

$L 用於字面量(Literals)

JavaPoet 提供的獨有的語法, $L表示取變量字面量值替換之(字面量概念google之):

private MethodSpec computeRange(String name, int from, int to, String op) {
  return MethodSpec.methodBuilder(name)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
      .addStatement("result = result $L i", op)
      .endControlFlow()
      .addStatement("return result")
      .build();
}

$S 用於字符串

$S表示取變量字面量值加雙引號後替換之,如下:

public static void main(String[] args) throws Exception {
  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();
}

在這個例子裏,使用$S會自動加上引號:

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

  String eminem() {
    return "eminem";
  }

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

$T 用於類型

如何在生成代碼時自動去import類型,僅僅需要使用$T來關聯類型:

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

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

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

javaFile.writeTo(System.out);

看如下代碼,.java文件裏自動加入了需要導入的Data類:

package com.example.helloworld;

import java.util.Date;

public final class HelloWorld {
  Date today() {
    return new Date();
  }
}

上個例子的Date.class是已經存在的,那麼如果我們需要關聯一個當前環境不存在類如何做?看下面:

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

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

這個類還不存在但是依然會導入:

package com.example.helloworld;

import com.mattel.Hoverboard;

public final class HelloWorld {
  Hoverboard tomorrow() {
    return new Hoverboard();
  }
}

ClassName非常重要,在使用JavaPoet的時候會經常用到。
JavaPoet可以表示任何_.class_類型外,還可以表示數組,參數類型,通配符類型,類型變量,如下:

ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);

MethodSpec beyond = MethodSpec.methodBuilder("beyond")
    .returns(listOfHoverboards)
    .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("return result")
    .build();

JavaPoet將會分解每個類型,且儘可能導入所有需要的類型。

package com.example.helloworld;

import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;

public final class HelloWorld {
  List<Hoverboard> beyond() {
    List<Hoverboard> result = new ArrayList<>();
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    return result;
  }
}

導入靜態類型(Import static)

JavaPoet 支持 import static. 通過明確的指定類型名,靜態導入的方式修改上面的代碼如下:

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

MethodSpec beyond = MethodSpec.methodBuilder("beyond")
    .returns(listOfHoverboards)
    .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
    .addStatement("result.add($T.createNimbus(2000))", hoverboard)
    .addStatement("result.add($T.createNimbus(\"2001\"))", hoverboard)
    .addStatement("result.add($T.createNimbus($T.THUNDERBOLT))", hoverboard, namedBoards)
    .addStatement("$T.sort(result)", Collections.class)
    .addStatement("return result.isEmpty() ? $T.emptyList() : result", Collections.class)
    .build();

TypeSpec hello = TypeSpec.classBuilder("HelloWorld")
    .addMethod(beyond)
    .build();

JavaFile.builder("com.example.helloworld", hello)
    .addStaticImport(hoverboard, "createNimbus")
    .addStaticImport(namedBoards, "*")
    .addStaticImport(Collections.class, "*")
    .build();

JavaPoet首先會添加import static,然後再添加其它需要的類型。

package com.example.helloworld;

import static com.mattel.Hoverboard.Boards.*;
import static com.mattel.Hoverboard.createNimbus;
import static java.util.Collections.*;

import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;

class HelloWorld {
  List<Hoverboard> beyond() {
    List<Hoverboard> result = new ArrayList<>();
    result.add(createNimbus(2000));
    result.add(createNimbus("2001"));
    result.add(createNimbus(THUNDERBOLT));
    sort(result);
    return result.isEmpty() ? emptyList() : result;
  }
}

$N 用於名稱

生成的代碼一般都會相互關聯。使用**$N**來關聯其它聲明。如下生成兩個方法,其一調用另一個方法:

public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

public char hexDigit(int i) {
  return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}

當我們生成上面的代碼時,傳遞hexDigit方法來替換$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();

代碼塊格式化字符串

代碼塊有多種方式爲佔位符指定值。

相對參數

給每個佔位符傳遞一個參數,個數和順序一一對應。在每個例子中,我們生成的代碼將輸出爲"I ate 3 tacos":

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

指定位置的參數

如下在佔位符前加入整數值來指定使用哪個參數,整數值從1開始。

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

指定名字的參數

使用語法$argumentName:X來指定參數名字,X是格式化字符,argumentName是關聯到一個 map 裏的。參數名使用字符a-z, A-Z, 0-9, and _,且必須以小寫字符開頭。

Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos");
map.put("count", 3);
CodeBlock.builder().addNamed("I ate $count:L $food:L", map)

方法

上面所有的方法都有方法體。可以使用 Modifiers.ABSTRACT讓方法沒有方法體,但是只有在封裝類是抽象類或接口。

MethodSpec flux = MethodSpec.methodBuilder("flux")
    .addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED)
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addMethod(flux)
    .build();

生成代碼如下:

public abstract class HelloWorld {
  protected abstract void flux();
}

JavaPoet 使用javax.lang.model.element.Modifier指定修飾符,但是這個類在Android上是不可用的。使用JavaPoet一般是創建Jar包,這樣就可以正常使用javax.lang.model.element.Modifier類,生成的代碼是可以在任何地方運行的:JVMs,Android和GWT。

方法也有參數,異常,可變參數,Java文檔,註解,類型變量和返回類型,所有這些都在MethodSpec.Builder裏配置。

構造器

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();

生成代碼如下:

public class HelloWorld {
  private final String greeting;

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

生成源文件時,JavaPoet 會把構造函數放在普通方法之前輸出到文件。

參數

在方法或構造函數上聲明參數時,可以使用ParameterSpec.builder()MethodSpec的APIaddParameter()

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();

上面代碼我們使用了不同的方式創建了androidrobot參數,最終輸出是一致的:

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

Builder方式創建參數還是很有必要的,比如說需要給參數添加註解(例如:@Nullable)。

屬性

想創建參數一樣,可以使用builder,也可以直接添加屬性:

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();

生成代碼如下:

public class HelloWorld {
  private final String android;

  private final String robot;
}

Builder方式創建屬性也是非常必要的,比如屬性有Java文檔,註解,屬性初始化。

FieldSpec android = FieldSpec.builder(String.class, "android")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S + $L", "Lollipop v.", 5.0d)
    .build();

生成代碼如下:

private final String android = "Lollipop v." + 5.0;

接口

JavaPoet 處理接口沒有問題,我們知道接口方法必須是PUBLIC ABSTRACT,接口屬性必須是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!")
            .returns(String.class)
            .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 String toString() {
      return "avalanche!";
    }
  },

  SCISSORS("peace"),

  PAPER("flat");

  private final String handsign;

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

匿名內部類

在枚舉代碼中我們使用了TypeSpec.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();
    }
  });
}

匿名內部類比較棘手的部分是傳遞參數給父類構造函數。以上例子都傳遞了空的字符串表示沒有參數:TypeSpec.anonymousClassBuilder("")。使用JavaPoet的代碼塊語法傳遞不同的參數,以逗號隔開。

註解

添加註解非常簡單:

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

生成一個帶@Override註解的方法:

  @Override
  public String toString() {
    return "Hoverboard";
  }

使用AnnotationSpec.builder()給註解設置屬性:

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();

生成代碼如下:

@Headers(
    accept = "application/json; charset=utf-8",
    userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);

更復雜的情況,註解嵌套,$L用於嵌套的註解:

MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addAnnotation(AnnotationSpec.builder(HeaderList.class)
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "Accept")
            .addMember("value", "$S", "application/json; charset=utf-8")
            .build())
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "User-Agent")
            .addMember("value", "$S", "Square Cash")
            .build())
        .build())
    .addParameter(LogRecord.class, "logRecord")
    .returns(LogReceipt.class)
    .build();

生成代碼如下:

@HeaderList({
    @Header(name = "Accept", value = "application/json; charset=utf-8"),
    @Header(name = "User-Agent", value = "Square Cash")
})
LogReceipt recordEvent(LogRecord logRecord);

你可以多次調用addMember(),傳遞相同的屬性名來填充一個屬性值列表。

Java文檔

屬性,方法和類型能使用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();

生成代碼如下:

  /**
   * 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);

Java文檔裏使用$T關聯類型可以自動導入。

下載

下載the latest .jar 或者通過Maven依賴:

<dependency>
  <groupId>com.squareup</groupId>
  <artifactId>javapoet</artifactId>
  <version>1.11.1</version>
</dependency>

或者 Gradle:

compile 'com.squareup:javapoet:1.11.1'

開發版本快照 Sonatype’s snapshots repository.

License

Copyright 2015 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

JavaWriter

JavaPoet is the successor to JavaWriter. New projects should prefer JavaPoet because
it has a stronger code model: it understands types and can manage imports automatically. JavaPoet is
also better suited to composition: rather than streaming the contents of a .java file
top-to-bottom in a single pass, a file can be assembled as a tree of declarations.

JavaWriter continues to be available in GitHub and Maven Central.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章