编译时动态生成代码技术之javapoet(四)
Javapoet简介
javapoet是android之神JakeWharton开源的一款快速代码生成工具,配合APT在项目编译期间动态生成代码,并且使用其API可以自动生成导包语句。这可以减少我们在项目开发中模板化代码的编写,减轻程序员开发所需要的时间,提高编码效率,这也是好的架构努力方向。
javapoet github链接:https://github.com/square/javapoet
核心类
JavaPoet定义了一系列类来尽可能优雅的描述java源文件的结构。观察JavaPoet的代码主要的类可以分为以下几种:
-
Spec 用来描述Java中基本的元素,包括类型,注解,字段,方法和参数等。
- AnnotationSpec
- FieldSpec
- MethodSpec
- ParameterSpec
- TypeSpec
-
Name 用来描述类型的引用,包括Void,原始类型(int,long等)和Java类等。
- TypeName
- ArrayTypeName
- ClassName
- ParameterizedTypeName
- TypeVariableName
- WildcardTypeName
- CodeBlock 用来描述代码块的内容,包括普通的赋值,if判断,循环判断等。
- JavaFile 完整的Java文件,JavaPoet的主要的入口。
- CodeWriter 读取JavaFile并转换成可阅读可编译的Java源文件。
MethodSpec介绍
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介绍
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")//构造一个类,类名
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)//定义类的修饰符
.addMethod(main)//添加类的方法,也就是上面生成的MethodSpec对象
.build();
JavaFile介绍
JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)//定义生成的包名,和类
.build();
javaFile.writeTo(System.out);//输出路径,可以收一个file地址
使用javapoet首先添加gradle依赖:
compile 'com.squareup:javapoet:1.11.1'
整理一下代码,如下
private void creatCode(){
MethodSpec main = MethodSpec.methodBuilder("mian")
.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("baijunyu.com.testelement", helloWorld).build();
try {
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
}
生成的代码,在控制台打印如下
在上面的代码中我们用MethodSpec定义了一个main函数,然后用addModifiers给代码添加了public和static修饰符,returns添加返回值,addParameter添加参数,addStatement给方法添加代码块。定义好方法之后我们就需要将方法加入类中,利用TypeSpec可以构建相应的类信息,然后将main方法通过addMethod()添加进去,如此就将类也构建好了。而JavaFile包含一个*的Java类文件,需要将刚刚TypeSpec生成的对象添加进去,通过这个JavaFile我们可以决定将这个java类以文本的形式输出或者直接输出到控制台。
- MethodSpec addCode和addStatement
addCode生成代码
MethodSpec main = MethodSpec.methodBuilder("main")
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < 10; i++) {\n"
+ " total += i;\n"
+ "}\n")
.build();
结果
一般的类名和方法名是可以被模仿书写的,但是方法中的构造语句是不确定的,这时候就可以用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占位符
占位符使一个字符串的拼接形式转化为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();
可以看出简化了不少,可以将代码中改变的部分0,10,* 等通过参数传入,而对于不变的直接使用javapoet生成。
2). $S 字符串
当我们想输出字符串文本时,我们可以使用 $S去输出一个字符串,而如果我们我们使用 $L 并不会帮我们加上双引号。
使用 $S:
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();
}
代码输出:
使用 $L代码输出结果:
3). $T 对象
对于我们Java开发人来说,JDK和SDK提供的各种java和android的API极大程度的帮助我们开发应用程序。对于Javapoet它也充分支持各种Java类型,包括自动生成导包语句,仅仅使用 $T 就可以了。
MethodSpec main = MethodSpec.methodBuilder("today")
.returns(Date.class)
.addStatement("return new $T()", Date.class)
.build();
代码输出结果:
以上是生成JDK和SDK导包语句,通过传入 Date.class来生成代码,如果想导入自定义的包和类怎么办?
我们可以直接通过反射获取一个ClassName对象,作为类传入
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
MethodSpec main = MethodSpec.methodBuilder("tomorrow")
.returns(hoverboard)
.addStatement("return new $T()", hoverboard)
.build();
代码输出结果:
Javpoet也可以支持静态导入,它通过显示地收集类型成员名来实现
JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)
.addStaticImport(hoverboard, "getAge")
.addStaticImport(hoverboard, "getName")
.addStaticImport(Collections.class, "*")
.build();
代码输出结果:
静态导入,我们只需要在JavaFile的builder中链式调用addStaticImport方法就可以,第一个参数为Classname对象,第二个为需要导入的对象中的静态方法。
import static 是静态导入,是jdk1.5的新特征.利用import static 可以不通过调用包名,直接使用包里的静态方法。
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();
try {
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
代码输出结果:
在上面例子中,byteToHex想要调用 hexDigit方法,我们就可以使用$ N来调用,hexDigit()方法作为参数传递给byteToHex()方法通过使用$N来达到方法自引用。
代码块格式字符串
CodeBlock的用法,代码块
AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get("baijunyu.com.test.poet", "MyAnnotation"));
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder().add("$S", "world");
builder.addMember("hello", codeBlockBuilder.build());
AnnotationSpec annotationSpec = builder.build();
MethodSpec.Builder methoBuilder = MethodSpec.methodBuilder("toString");
methoBuilder.addModifiers(Modifier.PUBLIC);
methoBuilder.returns(TypeName.get(String.class));
CodeBlock.Builder toStringCodeBuilder = CodeBlock.builder();
toStringCodeBuilder.beginControlFlow("if( hello != null )");
toStringCodeBuilder.add(CodeBlock.of("return \"hello \"+hello;\n"));
toStringCodeBuilder.nextControlFlow("else");
toStringCodeBuilder.add(CodeBlock.of("return \"\";\n"));
toStringCodeBuilder.endControlFlow();
methoBuilder.addCode(toStringCodeBuilder.build());
MethodSpec main = methoBuilder.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC)
.addMethod(main)
.addAnnotation(annotationSpec)
.build();
JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)
.build();
try {
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
代码输出结果:
相关参数格式化
CodeBlock codeBlock = CodeBlock.builder().add("I ate $L $L", 3, "ta").build();
通过位置参数指定要用的参数
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的key必须小写字母开头
map.put("food", "tacos");
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();
try {
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
代码输出结果:
方法参数
经常我们需要给方法加上入口参数,此时我们就可以通过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();
代码输出结果:
成员变量
我们可以通过 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可以初始化成员变量
FieldSpec android = FieldSpec.builder(String.class, "android")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("$S + $L", "Lollipop v.", 5.0d)
.build();
代码输出结果:
接口
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();
代码输出结果:
生成接口对象时,这些修饰符都会省略掉
如果不加编译会报错
枚举
使用enumBuilder 去创建一个枚举类型,使用addEnumConstant()去添加枚举常量值
TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("ROCK")
.addEnumConstant("SCISSORS")
.addEnumConstant("PAPER")
.build();
代码输出结果:
复杂一点的枚举
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();
代码输出结果:
在Android官方的性能优化相关课程中曾经提到使用枚举存在的性能问题,不建议在Android代码中使用枚举。为了弥补Android平台不建议使用枚举的缺陷,官方推出了两个注解,IntDef和StringDef,用来提供编译期的类型检查。
匿名内部类
对于匿名内部类,我们可以使用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();
代码输出结果:
注解
前文在介绍CodeBlock的时候已经用了一次 AnnotationSpec生成注解,用在类上,这里简单实例下方法上的注解,以@override为例,很简单
MethodSpec method = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.returns(String.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "Hoverboard")
.build();
代码输出结果:
javapoet的简单用法到此介绍完了,大部分都是github的官方示例,掌握了这些基本用法我相信对于阅读开源框架的代码会大有用处,对于我们开发自己的架构和框架更是必不可少
上一篇: Linux chage命令详解