基于JavaPoet自動(dòng)生成java代碼文件

要實(shí)現(xiàn)一個(gè)功能,我們通常編寫一系列的java文件,如果需求發(fā)生變化,則修改這些java文件或增加一些新的java文件。為了避免為適應(yīng)千變?nèi)f化的需求而頻繁修改項(xiàng)目代碼,可以在運(yùn)行時(shí)動(dòng)態(tài)生成字節(jié)碼,當(dāng)然運(yùn)行時(shí)生成字節(jié)碼需要占用計(jì)算資源。當(dāng)然,還有一種思路是根據(jù)條件動(dòng)態(tài)生成java文件,而不是根據(jù)每種情況編寫固定的代碼,這樣生成的項(xiàng)目與完全手工編寫的代碼沒有任何區(qū)別。JavaPoet就是一個(gè)動(dòng)態(tài)生成java文件的庫(kù),在caffine、butterknife、自動(dòng)生成rpc stub文件等中間件中得到了應(yīng)用。

什么是[AOP]

AOP面向切面編程,就是在代碼預(yù)編譯階段,在不修改源代碼的情況下,給程序添加某一功能。

像成熟的框架,ARouter,ButterKnife等也都使用了這個(gè)技術(shù)。任何技術(shù)的出現(xiàn)都有其實(shí)際應(yīng)用場(chǎng)景,為了解決某一方面的痛點(diǎn)。AOP的出現(xiàn)讓某些功能組件的封裝更加解耦,使用者能夠更加的方便的使用組件里的功能。

拿ButterKnife舉例,我們?cè)_發(fā),以前經(jīng)常寫很多findViewById的代碼,顯然這類代碼寫起來很繁瑣,且容易出錯(cuò)(id和view有時(shí)候沒對(duì)上)。而AOP可以有效避免這些問題。

比如我們可以通過在預(yù)編譯的階段解析注解,然后生成對(duì)應(yīng)的java文件,該java文件封裝了findviewbyid的方法,實(shí)現(xiàn)view和id的動(dòng)態(tài)綁定。這樣就非常有效減少了后期的編寫代碼的工作量,可以快速實(shí)現(xiàn)view和id的綁定操作。

javapoet的運(yùn)用

JavaPoet是square推出的開源java代碼生成框架,提供Java Api生成.java源文件。這個(gè)框架功能非常有用,我們可以很方便的使用它根據(jù)注解、數(shù)據(jù)庫(kù)模式、協(xié)議格式等來對(duì)應(yīng)生成代碼。通過這種自動(dòng)化生成代碼的方式,可以讓我們用更加簡(jiǎn)潔優(yōu)雅的方式要替代繁瑣冗雜的重復(fù)工作。引用依賴:

compile 'com.squareup:javapoet:1.7.0'

該項(xiàng)目結(jié)構(gòu)如下:


image.png

1 基本功能介紹

我們知道,一個(gè)java類由類聲明、字段、構(gòu)造方法、方法、參數(shù)、注解等元素組成,JavaPoet為這些基本組成元素分別定義了相應(yīng)的類,分別用來管理、生成相應(yīng)元素相關(guān)的代碼。

JavaPoet常用的一些類:

class 說明
JavaFile 對(duì)應(yīng)編寫的.java文件
TypeSpec 對(duì)應(yīng)一個(gè)類、接口或enum
MethodSpec 對(duì)應(yīng)一個(gè)方法或構(gòu)造方法
FieldSpec 對(duì)應(yīng)一個(gè)字段
ParameterSpec 對(duì)應(yīng)方法或構(gòu)造方法的一個(gè)參數(shù)
AnnotationSpec 對(duì)應(yīng)類型、字段、方法或構(gòu)造方法上的注解
ClassName 對(duì)應(yīng)一個(gè)類、接口或enum的名字,由package名字和類名字兩部分組成
CodeBlock 代碼塊,一般用來生成{}包裹起來的數(shù)據(jù)塊
ParameterizedTypeName 泛型中的參數(shù)化類型
TypeVariableName 泛型中的類型變量
WildcardTypeName 泛型中的通配符?

1.1 一個(gè)簡(jiǎn)單的例子

public class JavapoetApplication {

    public static void main(String[] args) {
        try {
            // 定義一個(gè)方法名為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();

            // 定義一個(gè)類,名字為TestCode
            TypeSpec testCode = TypeSpec.classBuilder("TestCode")
                    // 類修飾符
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    // 添加方法
                    .addMethod(test)
                    .build();

            // 定義一個(gè)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)例子

上面的自動(dòng)生成代碼中,雖然類和方法聲明、修飾符、返回值類型、包名等是用編程實(shí)現(xiàn)的(編程實(shí)現(xiàn)就意味這可以參數(shù)化,通過控制參數(shù)生成不同的內(nèi)容),但是方法體的內(nèi)容與手工編寫并沒有什么區(qū)別,像語(yǔ)句分號(hào)、縮進(jìn)、換行等都是人工編寫的,看不出自動(dòng)生成代碼的優(yōu)勢(shì)。但JavaPoet提供了addStatement()beginControlFlow(),endControlFlow(),nextControlFlow()等方法方便代碼生成。addStatement()會(huì)自動(dòng)在語(yǔ)句后添加分號(hào),并換行;beginControlFlow()nextControlFlow()會(huì)自動(dòng)添加{符號(hào)并換行,控制后面語(yǔ)句的縮進(jìn);endControlFlow()會(huì)自動(dòng)添加}符號(hào),并換行,控制后面語(yǔ)句的縮進(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)自動(dòng)生成代碼的靈活性,JavaPoet提供了L、S、T、N等替換符號(hào)來實(shí)現(xiàn)這方面的需求。

進(jìn)一步修改上面test方法的生成代碼,如下:

    // 定義一個(gè)方法名為test的方法
    // 定義一個(gè)參數(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 會(huì)替換為變量total的值
            .addStatement("int $L = 0", total)
            // $N 會(huì)替換為 loopParam的名字
            .beginControlFlow("for (int i = 0; i < $N; i++)", loopParam)
            .addStatement("$L += i", total)
            .endControlFlow()
            // $T 會(huì)替換為類的名字,如果需要,會(huì)在文件頭添加相應(yīng)的import語(yǔ)句
            // $S 會(huì)替換為變量的值,并用""包裹起來
            .addStatement("$T.out.println($S + $L)", System.class, "total value: ", total)
            .build();

1.4 替換符號(hào)功能說明

JavaPoet幾個(gè)常用替換符號(hào)的功能介紹如下:

替換符號(hào) 說明
$T 參數(shù)是Class對(duì)象,替換為Class的名字,如果需要,同時(shí)在文件頭添加相應(yīng)的import語(yǔ)句
$L 替換為變量值的字面量值,功能相當(dāng)于字符串的format()方法
$S 也是替換為變量值的字面量值,但是字面量值為被字符串雙引號(hào)包裹起來
$N 參數(shù)是ParameterSpec、TypeSpec、MethodSpec等,替換為這些變量的name值

替換符號(hào)還可以指定替換參數(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)

生成的語(yǔ)句都是

System.out.println("I ate 3 tacos");

2 JavaPoet個(gè)組件使用說明

2.1 類型

變量、參數(shù)、返回值的類型即可以用java的class來表達(dá),javapoet也定義了相應(yīng)的類型表示系統(tǒng),對(duì)應(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

字段的生成比較簡(jiǎn)單,只要指定字段的修飾符、類型、名字創(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成員
一個(gè)基本例子:

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
}

一個(gè)復(fù)雜一點(diǎn)的例子:

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文件?

在寫注解處理器時(shí),首先就是要繼承AbstractProcessor,并且按照如下步驟聲明:

  • 需要在 processors 庫(kù)的 main 目錄下新建 resources 資源文件夾;
  • 在 resources文件夾下建立 META-INF/services 目錄文件夾;
  • 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
  • 在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;

這樣聲明下來也太麻煩了?這就是用引入auto-service的原因。

在類的頂部加入注解:@AutoService(Processor.class),這個(gè)注解處理器是Google開發(fā)的,可以用來生成 META-INF/services/javax.annotation.processing.Processor 文件信息。

使用遇到的問題
在module_processor中導(dǎo)入我們要用的auto-service庫(kù);

implementation 'com.google.auto.service:auto-service:1.0-rc6'

在類上面添加service的注解即可:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
 ...}

編譯項(xiàng)目后卻始終不見META-INF目錄的生成,正常是會(huì)在該注解處理器項(xiàng)目的目錄module_processor/build/classes/java/main/META-INF下生成。

注意:可能是版本兼容問題,把版本降低一點(diǎn)

我們用到了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);
image.png

AutoService會(huì)自動(dòng)在META-INF文件夾下生成Processor配置信息文件,該文件里就是實(shí)現(xiàn)該服務(wù)接口的具體實(shí)現(xiàn)類。而當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,

就能通過該jar包META-INF/services/里的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化,完成模塊的注入。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容