前言

自从从事Android开发以来,一直做的应用层开发,代码写多了,感觉一直在一个瓶颈中,每天写代码无非就是调接口,填数据到页面,再就是做些简单的自定义View,写出产品经理希望的界面,然后就完事,也很少做些界面的调优和优化,一直想学习写java和android更深入的知识点,提升自己的知识技能….

闲话到此为止,最近突然看到一篇文章,这里是文章链接:Android 利用 APT 技术在编译期生成代码,看完真是感觉真是项目的好帮手,利用注解和javapoet工具在项目编译期间动态生成代码,减少我们在项目开发中模板化代码的编写,而我们现在很多开源框架如butterknife,内部就是利用类似工具来完成代码的动态注入和控件绑定。

到此就可以引入该篇文章的正式主题,利用javapoet优雅的生成我们项目的模板化代码,摆脱重复代码书写的困扰,减轻程序员开发所需要的时间,提高编码效率。
javapoet是android之神JakeWharton开源的一款快速代码生成工具,配合APT简直就是爽得不行不行的,并且使用其API可以自动生成导包语句。

接下来配合github文档,总结下其使用方法!

HelloWorld Example

作为一个第三方的工具,第一步肯定要添加gradle依赖:

compile ‘com.squareup:javapoet:1.9.0’

然后就是Java程序员入门的第一个例子

package com.example.helloworld;

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

想必只要是个程序员都写过这样的例子,但是我们用javapoet生成是这样的:

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)
    .addMethod(main)
    .build();

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

javaFile.writeTo(System.out);

这段代码执行的结果就是上面java helloworld方法,在方法里面我们用MethodSpec定义了一个main函数,然后用addModifiers给代码添加了public和static修饰符,returns添加返回值,addParameter添加参数,addStatement给方法添加代码块,可能addStatement里面的内容有点难以理解,不过只要把$S$T当成占位符,整个结构想象成String.format()就好理解了。定义好方法之后我们就需要将方法加入类中,利用TypeSpec可以构建相应的类信息,然后将main方法通过addMethod()添加进去,如此就将类也构建好了。而JavaFile包含一个顶级的Java类文件,需要将刚刚TypeSpec生成的对象添加进去,通过这个JavaFile我们可以决定将这个java类以文本的形式输出或者直接输出到控制台。

看完第一个例子可能有点感觉,有点意思,以代码来生成代码,嗯 。

接着看另一个Example

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

我们可以看到MethodSpec中有个addCode新方法,接下来就简单解释下addCode

  • addCode和addStatement

一般的类名和方法名是可以被模仿书写的,但是方法中的构造语句是不确定的,这时候就可以用addCode来添加代码块来实现此功能。但是addCode也有其缺点,我们将第一个helloworld用addCode和addStatement生成的代码对比看下

//addCode生成的
package com.example.helloworld;

import java.lang.String;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println('Hello World');
  }
}
//addStatement生成的
package com.example.helloworld;

import java.lang.String;
import java.lang.System;

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

可以看出用addStatement生成的多了一行导包语句,也就是说用addStatement生成的代码可以自动同时生成导包代码语句,同时使用addStatement可以减少手工的分号,换行符,括号书写,直接使用Javapoet的api时,代码的生成简单得不要不要的。

我么也可以使用beginControlFlow() + endControlFlow(),替代for循环的代码拼写方式,比如这样:

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

我们也可以使用拼接的方式,动态的传递循环次数控制比如这样:

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

但是既然我们使用了javapoet,就可以使用其API来替换这种字符拼接模式,从而维护代码的可阅读性。

javapoet占位符

占位符,是一个很重要的概念,无论是javaweb数据库Dao层数据库操作的占位符概念,还是Hibernate中占位符,都用到了这个概念,现在javapoet任然可以使用,占位符使一个字符串的拼接形式转化为String.format的格式,提高代码的可读性。

现在来介绍下javapoet中几个常用的占位符。

1). $L 文本值

对于字符串的拼接,在使用时是很分散的,太多的拼接的操作符,不太容易观看。为了去解决这个问题,Javapoet提供一个语法,它接收$L去输出一个文本值,就像String.format(),这个$L可以是字符串,基本类型。

比如上面使用字符串拼接的改为$L来看下:

MethodSpec main = MethodSpec.methodBuilder("main")
                .returns(int.class)
                .addStatement("int result = 0")
                .beginControlFlow("for (int i = $L; i < $L; i++)", 0, 10)
                .addStatement("result = result $L i", '*')
                .endControlFlow()
                .addStatement("return result")
                .build();

可以看出简化了不少,我们可以将代码中改变的部分通过参数传入,而对于不变的直接使用javapoet生成。

2). $S 字符串

当我们想输出字符串文本时,我们可以使用 $S去输出一个字符串,而如果我们我们使用 $L 并不会帮我们加上双引号,比如:

public static void test() throws IOException {
        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();
    }

这段代码的输出是这样的:

package com.example.helloworld;

import java.lang.String;

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

  String eminem() {
    return "eminem";
  }

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

而如果使用 $L的输出是这样的

...
  String slimShady() {
    return slimShady;
  }

  String eminem() {
    return eminem;
  }

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

这个编译的时候肯定会报错的。

3). $T 对象

对于我们Java开发人来说,JDK和SDK提供的各种java和android的API极大程度的帮助我们开发应用程序。对于Javapoet它也充分支持各种Java类型,包括自动生成导包语句,仅仅使用 $T 就可以了。

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

我们通过传入 Date.class来生成代码,我们也可以直接通过反射获取一个ClassName对象,然后传入。

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

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

简直屌爆了….., 好吧,我们也只能用这轮子了。

静态导入

Javpoet也可以支持静态导入,它通过显示地收集类型成员名来实现,让我们还是看例子吧。

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
       .addStaticImport(hoverboard, "chen")
       .addStaticImport(hoverboard, "xiao")
       .addStaticImport(Collections.class, "*")
       .build();

静态导入,我们只需要在JavaFile的builder中链式调用addStaticImport方法就可以,第一个参数为Classname对象,第二个为需要导入的对象中的静态方法。我们应该根据需要匹配和调整所有的调用,并导入所有其他类型。

4).$N 名字

有时候生成的代码是我们自己需要引用的,这时候可以使用 $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();

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

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

javaFile.writeTo(System.out);

在上面例子中,byteToHex想要调用 hexDigit方法,我们就可以使用 $N来调用,hexDigit()方法作为参数传递给byteToHex()方法通过使用$N来达到方法自引用。

代码块格式字符串

相关参数格式化

CodeBlock codeBlock = CodeBlock.builder().add("I ate $L $L", 3, "ta").build();
System.out.println(codeBlock.toString());

输出:

I ate 3 ta

我们也可以通过位置参数指定要用的参数的位置:

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

在 $的后面指定需要的参数位置序号,非常方便。

名字参数

通过$argumentName:X这样的语法形式来达到通过key名字寻找值,然后使用的功能,参数名可以使用 a-z, A-Z, 0-9, and _ ,但是必须使用小写字母开头。

Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos"); //map的key必须小写字母开头
map.put("count", 3);
CodeBlock.builder().addNamed("I ate $count:L $food:L", map)

构造方法

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

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

javaFile.writeTo(System.out);

输出:

package com.example.helloworld;

import java.lang.String;

public class HelloWorld {
  private final String greeting;

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

还是很好理解的

方法参数

经常的我们需要给方法加入口参数,此时我们就可以通过ParameterSpec来达到这一目的

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

输出:

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

成员变量

我们可以通过 Fields来达到生成成员变量的作用

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

initializer可以初始化成员变量,比如这样:

.initializer("$S + $L", "Lollipop v.", 5.0d)

各种姿势,有木有~~~

接口

Javpoet中的接口方法必须始终用PUBLIC ABSTRACT修饰符修饰,而对于字段Field必须用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!")
            .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 void toString() {
      return "avalanche!";
    }
  },

  SCISSORS("peace"),

  PAPER("flat");

  private final String handsign;

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

还是很方便的有木有~~~

匿名内部类

对于匿名内部类,我们可以使用Types.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();
    }
  });
}

注解

注解也是被支持的,这里有个小例子

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

还是很容易懂的。

最后

这篇文章大部分例子都是github官方介绍的,有不懂的可以直接去官网看。用了一段时间javapoet感觉还是很有用的,最近项目一直写些重复性很大的代码,但是又是两个人开发的,并且代码也很多,所以想改动起来还是很麻烦的,但是还是先了解下,以后说不定哪天会用到的。

本篇文章到这里基本就结束了,下篇想详细介绍下注解的使用。

参考

1.https://github.com/square/javapoet#code-block-format-strings
2.http://www.jianshu.com/p/95f12f72f69a