×

javapoet——让你从重复无聊的代码中解放出来

96
尸情化异
2016.05.20 01:09* 字数 1817

前言

之前写了一篇文章,是关于butterknife源码解析,最后发现在源代码最后用了开源项目javapoet来生成.java源代码,感觉很是有趣,所以最近闲下来了,就来研究研究javapoet的用法。

javapoet简介

正如其名,poet指诗人,也就是作诗的人。java poet指的是能够自动写java源代码的人。是不是感觉很有意思?

当我们在处理注解或元数据文件的时候,往往有自动生成源代码的需要。比如我前面写的一篇文章:java基础之注解,里面讲到了一个例子,在运行时通过反射处理注解,为我们实例化控件并添加点击事件,然而这种方法很大的一个缺点就是用了反射,导致app性能下降。其实我们完全可以通过接下来要讲的内容来在编译期间自动生成一个viewHolder.javaviewHolder里面的内容是实例化view控件并为其添加上点击事件。 这样就不需要再用反射,那么也就解决了性能问题。然而这个viewHolder.java是在编译期间生成的,也就和我们手动写的.java源文件一样。

在上面这个例子里面,通过编译期间自动生成的代码,你无需编写样板,无需再一次又一次地写findViewByIdaddOnClickListener。把用在这部分上的时间用在解决其他问题上,是不是也就提高了我们的效率呢?其实自动生成源代码的事情也就是javapoet要帮我们做的。

Example

talk is cheap show me the code

下面我们就来看一下javapoet的用法:

程序员的入门代码~~~那我们就来用这个魔法师来生成我们想要的Helloworld。

在贴代码前,我不得不先讲一下javapoet里面常用的几个类:

  • MethodSpec 代表一个构造函数或方法声明。
  • TypeSpec 代表一个类,接口,或者枚举声明。
  • FieldSpec 代表一个成员变量,一个字段声明。
  • JavaFile包含一个顶级类的Java文件。

好了,现在HelloWorld里面需要了解的类全部在上面了;下面就可以上代码了。

private void generateHelloworld() throws IOException{
        MethodSpec main = MethodSpec.methodBuilder("main") //main代表方法名
                    .addModifiers(Modifier.PUBLIC,Modifier.STATIC)//Modifier 修饰的关键字
                .addParameter(String[].class, "args") //添加string[]类型的名为args的参数
                    .addStatement("$T.out.println($S)", System.class,"Hello World")//添加代码,这里$T和$S后面会讲,这里其实就是添加了System,out.println("Hello World");
                .build();
                TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")//HelloWorld是类名
                .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                .addMethod(main)  //在类中添加方法
                .build();
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
                .build();
        javaFile.writeTo(System.out);
    }

这里,addStatement()方法的参数有点麻烦,先不用管。

运行下试试:

看来还是挺简单的嘛,轻松掌握。

接下来继续看:

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

那么现在问题来了,为什么有addCode()方法,还存在前面addStatement()这么麻烦的方法呢?

那我们就来用addCode()来实现上面的HelloWorld试试:

    private static void generateHelloworld() throws IOException {
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addParameter(String[].class, "args")
                .addCode("System.out.println(\"Hello World\");\n")
                .build();
        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                .addMethod(main).build();
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec).build();
        javaFile.writeTo(System.out);
    }

下面是运行结果,大家发现什么不对没有?

package com.example.helloworld;

import java.lang.String;

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

对,没错,就是少了一行

import java.lang.System;

好像也没什么不对呀,有没有这行代码似乎都能正常运行呀。

要是这样想你就错了,java.lang下面的包是自动隐性导入的,不需要我们手动再import一遍,所以你把生成的代码直接拿去运行是没有错的。

我们再想象一下,假如这里System.class不在java.lang下面,而在我们自定义的包下面或者jdk的其他不自动导入的包下面,这里是不是要报错呢???

有没有发现,其实我们自动生成的代码是自动缩进的,自动加分号和换行的??

扯远了,下面我们来看上面生成的for循环的另一种实现方式:

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

这种实现方式是不是很优雅? beginControlFlow一个控制流语句,if,for,while什么的写起来也是这么简单。

我们再来看一个更加有意思的东西,我们在computeRange 方法中来生成一个MethodSpec

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 = " + from + "; i < " + to + "; i++)")
      .addStatement("result = result " + op + " i")
      .endControlFlow()
      .addStatement("return result")
      .build();
}

我们在代码中,这样来调用试试呢:

        TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
            .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
            .addMethod(computeRange("multiply10to20", 10, 20, "*"))
            .build();
    JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
            .build();
    javaFile.writeTo(System.out);

看下生成的代码:

package com.example.helloworld;

public final class HelloWorld {
  int multiply10to20() {
    int result = 0;
    for (int i = 10; i < 20; i++) {
      result = result * i;
    }
    return result;
  }
}

改变下computeRange(String name, int from, int to, String op)的参数试试,你会发现生成的代码作用又不一样。

一颗赛艇,通过参数用方法来生成方法; 其实这里还有很多有趣的东西,大家可以慢慢去发现。

其实JavaPoet产生的是源代码,而不是字节码,你也可以通过读取检验它,以确保它是正确的。

其实上面的computeRange已经可以完成普通for循环的操作了,但是我们都知道String是不可变的,那么像我这样的人就有那么点强迫症,就想用String.format()来代码"result = result " + op + " i"这种代码。
其实JavaPoet也提供了一种方式来解决这个问题,那就是$L(其实我感觉这个玩意儿还有其他好的用途,原谅我愚钝,现在只发现了这么一个用途)。
$L代表的是字面量。我们可以用下面的代码来替换掉上面的

.beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)")
.addStatement("result = result " + op + " i")

那就是:

.beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
.addStatement("result = result $L i", op)

看起来是不是舒服多了?

其实除了**$L**,javapoet还提供了其他的占位符,比如:

  • $S for Strings
  • $T for Types
  • $N for Names(我们自己生成的方法名或者变量名等等)

这里的$T,在生成的源代码里面,也会自动导入你的类。

要是你还想import static block 到代码中,其实这也不是事儿,我们是需要要代码中addStaticImport

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

那么我们生成的类就会出现这些代码:

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

是不是非常方便呀?

其实有时候我们除了要生成方法和类外,还要在类中添加一些变量,那么这时候要怎么做呢?
其实上面我已经介绍过了,就是用FieldSpec类:

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

这里定义了一个android的变量,然后在TypeSpec 中加入:

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 = "Lollipop v." + 5.0;
  private final String robot;
}

其实javapoet还提供了很多的API供我们调用,基本上实现了所有我们能想到的例子,比如:

  • MethodSpec .addJavadoc("XXX") 方法上面添加注释
  • MethodSpec.constructorBuilder() 构造器
  • MethodSpec.addAnnotation(Override.class); 方法上面添加注解
  • TypeSpec.enumBuilder("XXX") 生成一个XXX的枚举
  • TypeSpec.interfaceBuilder("HelloWorld")生成一个HelloWorld接口 ==!

我们在程序中完全可以通过这些方法来动态生成一些我们想要的类。比如前面就的ButterknifeViewHolder类就是这样生成的,我们在android开发的时候,相信大多数开发者都用过Butterknife吧,它确实通过动态生成代码的方式让我们从重复无聊的代码中解放了出来。其实我们用javapoet可以做的还不止这些,javapoet还有更有趣的使用途径等待大家一起去发现呢。

android那点事
Web note ad 1