JavaPoet - 优雅地生成代码

Posted everlastxgb

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaPoet - 优雅地生成代码相关的知识,希望对你有一定的参考价值。

JavaPoet - 优雅地生成代码


一、项目简介

JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。

项目主页及源码:https://github.com/square/javapoet

二、项目总览

该项目代码量相对较小,只有一个package(com.squareup.javapoet),所有类均位于该package下。

2.1 大体结构图

2.2 关键类说明

class说明
JavaFileA Java file containing a single top level class用于构造输出包含一个顶级类的Java文件
TypeSpecA generated class, interface, or enum declaration生成类,接口,或者枚举
MethodSpecA generated constructor or method declaration生成构造函数或方法
FieldSpecA generated field declaration生成成员变量或字段
ParameterSpecA generated parameter declaration用来创建参数
AnnotationSpecA generated annotation on a declaration用来创建注解

在JavaPoet中,JavaFile是对.java文件的抽象,TypeSpec是类/接口/枚举的抽象,MethodSpec是方法/构造函数的抽象,FieldSpec是成员变量/字段的抽象。这几个类各司其职,但都有共同的特点,提供内部Builder供外部更多更好地进行一些参数的设置以便有层次的扩展性的构造对应的内容。

另外,它提供$L(for Literals), $S(for Strings), $T(for Types), $N(for Names)等标识符,用于占位替换。

三、相关使用

3.1 API使用

关于JavaPoet 的API使用,官方Github主页已经有很详细的使用说明和示例了,具体可前往查看。此处不赘述,详见 项目主页、源码及使用说明

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

上方的代码是通过下方代码调用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);

四、源码浅析

下面来看看调用了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,记录下来后第二步才跟随内容一起写到CodeWriter。

另外我们可以看到源码中的emit方法,通过查看其它源码发现,在JavaPoet中,所有java文件的抽象元素都定义了emit方法,如TypeSepc,ParameterSepc等,emit方法传入CodeWriter对象输出字符串。上层元素调用下层元素的emit方法,如JavaFile的emit方法调用TypeSpec的emit方法,从而实现整个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通过调用codeWriter的emit方法依次输出javadoc,annotation,parameter,codeblock等。

五、使用场景

5.1 根据编译时注解生成代码

5.1.1 前言

用过butterknife的同学会发现,使用butterknife我们可以省去平时重复书写的findViewById之类的代码,通过注解的方式即可实现。而早期的butterknife使用的注解是运行时注解,即运行时通过注解然后使用反射实现,存在一定的性能问题,后面作者做了改进,使用编译时注解,编译期间,在注解处理器中对注解进行处理生成相应代码。

通过查看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 一个简单示例

本节将简单演示利用编译时注解+JavaPoet来实现编译期间动态生成代码。

工程目录结构:

  • Hello
    • app
    • hello-annotation (注解相关)
    • hello-compiler (处理器生成代码相关)

①. 导入依赖:

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相关,将 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();
    

④. 使用注解并调用生成的类函数
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是不存在的,这里会报错。那么,我们尝试编译一下,就会发现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 根据协议文件生成对应代码

假设我们对类的声明以及接口的声明是以特定格式写在一个协议文件中,那么我们可以先读取该协议文件内容,使用JavaPoet根据协议对应生成Java代码。

如定义以下协议文件:

service TestDemo 
    rpc doRequest (MyRequest) returns (MyResponse)  // 请求接口定义
    

    message MyRequest  // 请求内容实体
        string content;
    

    message MyResponse  // 返回内容实体
        int32 status_code;
        string entity;
    

那么利用JavaPoet我们可以生成对应的TestDemo.java, MyRequest.java, MyResponse.java, 以及TestDemo.java中对应的请求接口和实现。

注:此部分协议定义参考自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): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类如Elements, Types和Filer等。
  • process(Set< ? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。
  • getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
  • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。

注: 注解处理器是运行在独立的虚拟机JVM中,javac启动一个完整Java虚拟机来运行注解处理器。

6.1.2 注册注解处理器

那么,如何将我们自定义的处理器MyProcessor注册到javac中呢?首先我们需要将我们的注解处理器打包到一个jar文件中,其次在这个jar中,需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下。以下是这个jar的大致结构示意图:

  • MyProcessor.jar
    • com
      • example
        • MyProcessor.jar
    • META-INF
      • services
        • javax.annotation.processing.Processor

打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是,注解处理器的合法的全名列表,每一个元素换行分割:

com.example.MyProcessor  
com.foo.OtherProcessor  
net.blabla.SpecialProcessor  

把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册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相关,将原先的 apt project(':xxx-compiler') 改为 annotationProcessor project(':xxx-compiler') 即可。

七、小结

  • JavaPoet为square出品,并且诸如butterknife、Dagger等著名开源框架也使用该库,可见其质量保障性和稳定性。
  • JavaPoet提供的api清晰明了,使用起来简单方便,功能方面也很齐全,发布了很久目前也已迭代了很多个版本,趋于稳定阶段。
  • 运用JavaPoet预生成代码的方式,在省去我们频繁书写重复代码的同时,也避免了使用运行时反射造成的效率问题。

八、参考资料

以上是关于JavaPoet - 优雅地生成代码的主要内容,如果未能解决你的问题,请参考以下文章

使用 JavaPoet 生成注释

Android 组件化路由组件 ( 注解处理器中使用 JavaPoet 生成代码 )

深入理解Java注解——JavaPoet使用

深入理解Java注解——JavaPoet使用

深入理解Java注解——JavaPoet使用

JavaPoet使用攻略