前言
自从从事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