JavaPoet - 優(yōu)雅地生成代碼

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

JavaFile.png

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
    • META-INF
      • services
        • javax.annotation.processing.Processor

打包進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預生成代碼的方式,在省去我們頻繁書寫重復代碼的同時,也避免了使用運行時反射造成的效率問題。

八、參考資料

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

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

  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,157評論 0 3
  • 前面寫了Android 開發(fā):由模塊化到組件化(一),很多小伙伴來問怎么沒有Demo啊?之所以沒有立刻放demo的...
    涅槃1992閱讀 8,229評論 4 37
  • 本文章涉及代碼已放到github上annotation-study 1.Annotation為何而來 What:A...
    zlcook閱讀 29,786評論 15 116
  • Jake Wharton 是 Android 大神,同時也是開源狂魔。他開源的項目特點是小而美,且應用廣泛,比如 ...
    geniusmart閱讀 12,403評論 3 79
  • “這一部分的內(nèi)容為「設(shè)計原則-對比」,本章中僅將涉及內(nèi)容獨立講解。所有的示例只是為了闡述一個方面,如果有不足指出請...
    Vsplorer閱讀 412評論 0 0

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