要实现一个功能,我们通常编写一系列的java文件,如果需求发生变化,则修改这些java文件或增加一些新的java文件。为了避免为适应千变万化的需求而频繁修改项目代码,可以在运行时动态生成字节码,当然运行时生成字节码需要占用计算资源。当然,还有一种思路是根据条件动态生成java文件,而不是根据每种情况编写固定的代码,这样生成的项目与完全手工编写的代码没有任何区别。JavaPoet就是一个动态生成java文件的库,在caffine、butterknife、自动生成rpc stub文件等中间件中得到了应用。
什么是[AOP]
AOP面向切面编程,就是在代码预编译阶段,在不修改源代码的情况下,给程序添加某一功能。
像成熟的框架,ARouter,ButterKnife等也都使用了这个技术。任何技术的出现都有其实际应用场景,为了解决某一方面的痛点。AOP的出现让某些功能组件的封装更加解耦,使用者能够更加的方便的使用组件里的功能。
拿ButterKnife举例,我们原生开发,以前经常写很多findViewById的代码,显然这类代码写起来很繁琐,且容易出错(id和view有时候没对上)。而AOP可以有效避免这些问题。
比如我们可以通过在预编译的阶段解析注解,然后生成对应的java文件,该java文件封装了findviewbyid的方法,实现view和id的动态绑定。这样就非常有效减少了后期的编写代码的工作量,可以快速实现view和id的绑定操作。
javapoet的运用
JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。引用依赖:
compile 'com.squareup:javapoet:1.7.0'
该项目结构如下:
1 基本功能介绍
我们知道,一个java类由类声明、字段、构造方法、方法、参数、注解等元素组成,JavaPoet为这些基本组成元素分别定义了相应的类,分别用来管理、生成相应元素相关的代码。
JavaPoet常用的一些类:
class | 说明 |
---|---|
JavaFile | 对应编写的.java文件 |
TypeSpec | 对应一个类、接口或enum |
MethodSpec | 对应一个方法或构造方法 |
FieldSpec | 对应一个字段 |
ParameterSpec | 对应方法或构造方法的一个参数 |
AnnotationSpec | 对应类型、字段、方法或构造方法上的注解 |
ClassName | 对应一个类、接口或enum的名字,由package名字和类名字两部分组成 |
CodeBlock | 代码块,一般用来生成{} 包裹起来的数据块 |
ParameterizedTypeName | 泛型中的参数化类型 |
TypeVariableName | 泛型中的类型变量 |
WildcardTypeName | 泛型中的通配符? |
1.1 一个简单的例子
public class JavapoetApplication {
public static void main(String[] args) {
try {
// 定义一个方法名为test的方法
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修饰符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值类型
.returns(void.class)
// 方法的参数
.addParameter(Integer.class, "loop")
// 方法body内容
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < loop; i++) {\n"
+ " total += i;\n"
+ "}\n"
+ "System.out.println(\"total value: \" + total);\n")
.build();
// 定义一个类,名字为TestCode
TypeSpec testCode = TypeSpec.classBuilder("TestCode")
// 类修饰符
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
// 添加方法
.addMethod(test)
.build();
// 定义一个java文件,指定package和类定义
JavaFile javaFile = JavaFile.builder("com.javatest.javapoet", testCode)
.build();
// 将java文件内容写入文件中
File file = new File("./javapoet");
javaFile.writeTo(file);
} catch (Exception e) {
//
}
}
public static void test1() throws Exception {
}
}
生成的java文件内容为:
package com.javatest.javapoet;
import java.lang.Integer;
public final class TestCode {
public static void test(Integer loop) {
int total = 0;
for (int i = 0; i < loop; i++) {
total += i;
}
System.out.println("total value: " + total);
}
}
1.2 改进例子
上面的自动生成代码中,虽然类和方法声明、修饰符、返回值类型、包名等是用编程实现的(编程实现就意味这可以参数化,通过控制参数生成不同的内容),但是方法体的内容与手工编写并没有什么区别,像语句分号、缩进、换行等都是人工编写的,看不出自动生成代码的优势。但JavaPoet提供了addStatement()
,beginControlFlow()
,endControlFlow()
,nextControlFlow()
等方法方便代码生成。addStatement()
会自动在语句后添加分号,并换行;beginControlFlow()
和nextControlFlow()
会自动添加{
符号并换行,控制后面语句的缩进;endControlFlow()
会自动添加}
符号,并换行,控制后面语句的缩进。
重新编写上面test方法的生成代码,生成的结果代码是一样的:
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修饰符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值类型
.returns(void.class)
// 方法的参数
.addParameter(Integer.class, "loop")
// 方法body内容
.addStatement("int total = 0")
.beginControlFlow("for (int i = 0; i < loop; i++)")
.addStatement("total += i")
.endControlFlow()
.addStatement("System.out.println(\"total value: \" + total)")
.build();
1.3 进一步改进例子
现在生成代码的格式控制交给javaposet管理了,但是方法体的代码全是硬编码的,还没体现自动生成代码的灵活性,JavaPoet提供了L、S、T、N等替换符号来实现这方面的需求。
进一步修改上面test方法的生成代码,如下:
// 定义一个方法名为test的方法
// 定义一个参数
ParameterSpec loopParam = ParameterSpec.builder(Integer.class, "loop")
.addModifiers(Modifier.FINAL)
.build();
String total = "total";
MethodSpec test = MethodSpec.methodBuilder("test")
// 方法的修饰符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
// 方法的返回值类型
.returns(void.class)
// 方法的参数
.addParameter(loopParam)
// 方法body内容
// $L 会替换为变量total的值
.addStatement("int $L = 0", total)
// $N 会替换为 loopParam的名字
.beginControlFlow("for (int i = 0; i < $N; i++)", loopParam)
.addStatement("$L += i", total)
.endControlFlow()
// $T 会替换为类的名字,如果需要,会在文件头添加相应的import语句
// $S 会替换为变量的值,并用""包裹起来
.addStatement("$T.out.println($S + $L)", System.class, "total value: ", total)
.build();
1.4 替换符号功能说明
JavaPoet几个常用替换符号的功能介绍如下:
替换符号 | 说明 |
---|---|
$T | 参数是Class对象,替换为Class的名字,如果需要,同时在文件头添加相应的import语句 |
$L | 替换为变量值的字面量值,功能相当于字符串的format()方法 |
$S | 也是替换为变量值的字面量值,但是字面量值为被字符串双引号包裹起来 |
$N | 参数是ParameterSpec、TypeSpec、MethodSpec等,替换为这些变量的name值 |
替换符号还可以指定替换参数的位置(Relative Arguments)或参数的名字(Named Arguments)
例如:
addStatement("System.out.println(\"I ate $L $L\")", 3, "tacos")
和
# 指定参数位置
addStatement("System.out.println(\"I ate $2L $1L\")", "tacos", 3)
以及
# 指定参数名字
Map map = new LinkedHashMap<>();
map.put("food", "tacos");
map.put("count", 3);
addStatement("System.out.println(\"I ate $count:L $food:L\")", map)
生成的语句都是
System.out.println("I ate 3 tacos");
2 JavaPoet个组件使用说明
2.1 类型
变量、参数、返回值的类型即可以用java的class来表达,javapoet也定义了相应的类型表示系统,对应关系如下:
类别 | 生成的类型举例 | javapoet表达方式 | java class表达方式 |
---|---|---|---|
基本类型 | int | TypeName.INT | int.class |
基本类型包装类型 | TypeName.BOXED_INT | Integer.class | |
数组 | int[] | ArrayTypeName.of(int.class) | int[].class |
自定义类型 | TestCode.class | ||
参数化类型 | List |
ParameterizedTypeName.get(List.class, String.class) | |
类型变量 | T | TypeVariableName.get("T") | |
通配符类型 | ? extends String | WildcardTypeName.subtypeOf(String.class) |
2.2 field
字段的生成比较简单,只要指定字段的修饰符、类型、名字创建FieldSpec,然后添加到TypeSpec中就可以了
// 显式创建FieldSpec
FieldSpec android = FieldSpec.builder(String.class, "description")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
// 指定初始化值,可选
.initializer("$S", "this is a example")
.build();
ParameterizedTypeName type = ParameterizedTypeName.get(List.class, String.class);
FieldSpec android = FieldSpec.builder(type, "name").build();
分别生成:
private final String description = "this is a example";
List name;
2.3 class
TypeSpec.classBuilder("Clazz")
// 抽象类
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
// 泛型
.addTypeVariable(TypeVariableName.get("T"))
// 继承与接口
.superclass(String.class)
.addSuperinterface(Serializable.class)
.addSuperinterface(ParameterizedTypeName.get(Comparable.class, String.class))
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(Map.class),
TypeVariableName.get("T"),
WildcardTypeName.subtypeOf(String.class)))
// 初始化块
.addStaticBlock(CodeBlock.builder().build())
.addInitializerBlock(CodeBlock.builder().build())
// 添加字段
// .addField(fieldSpec)
// 构造方法和方法
// .addMethod(constructorSpec)
// .addMethod(methodSpec)
// 内部类
.addType(TypeSpec.classBuilder("InnerClass").build())
.build();
2.2 interface
通过TypeSpec的interfaceBuilder()
方法创建interface,其他元素添加跟class差不多
TypeSpec helloWorld = TypeSpec.interfaceBuilder("TestInterface").build();
2.3 enum
通过TypeSpec的enumBuilder()
方法创建interface, 通过addEnumConstant()
添加enum成员
一个基本例子:
TypeSpec helloWorld = TypeSpec.enumBuilder("TestEnum")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("EXAM_0")
.addEnumConstant("EXAM_1")
.addEnumConstant("EXAM_2")
.build();
生成的代码如下:
public enum TestEnum {
EXAM_0,
EXAM_1,
EXAM_2
}
一个复杂一点的例子:
TypeSpec testEnum = TypeSpec.enumBuilder("TestEnum")
.addModifiers(Modifier.PUBLIC)
.addEnumConstant("EXAM_0", TypeSpec.anonymousClassBuilder("$S", "exam0")
.addMethod(MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "exam0")
.returns(String.class)
.build())
.build())
.addEnumConstant("EXAM_1", TypeSpec.anonymousClassBuilder("$S", "exam1")
.build())
.addEnumConstant("EXAM_2", TypeSpec.anonymousClassBuilder("$S", "exam2")
.build())
.addField(String.class, "name", Modifier.PRIVATE, Modifier.FINAL)
.addMethod(MethodSpec.constructorBuilder()
.addParameter(String.class, "name")
.addStatement("this.$N = $N", "name", "name")
.build())
.build();
生成如下代码:
public enum TestEnum {
EXAM_0("exam0") {
@Override
public String toString() {
return "exam0";
}
},
EXAM_1("exam1"),
EXAM_2("exam2");
private final String name;
Roshambo(String name) {
this.name = name;
}
}
2.4 匿名内部类
使用TypeSpec的anonymousClassBuilder()
方法生成匿名内部类,举例如下:
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 testCode = TypeSpec.classBuilder("TestCode")
.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 strings) {
Collections.sort(strings, new Comparator() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
}
2.5 annotation
给方法增加基本注解:
MethodSpec toString = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.returns(String.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return $S", "testCode")
.build();
给方法增加带参数值的注解:
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(AnnotationSpec.builder(Headers.class)
.addMember("accept", "$S", "application/json; charset=utf-8")
.addMember("userAgent", "$S", "Square Cash")
.build())
.addParameter(LogRecord.class, "logRecord")
.returns(LogReceipt.class)
.build();
2.6 javadoc
类和方法都可以通过TypeSpec和MethodSpec的addJavadoc()
方法添加javadoc
MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
.addJavadoc("Hides {@code message} from the caller's history. Other\n"
+ "participants in the conversation will continue to see the\n"
+ "message in their own history unless they also delete it.\n")
.addJavadoc("\n")
.addJavadoc("Use {@link #delete($T)} to delete the entire\n"
+ "conversation for all participants.\n", Conversation.class)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addParameter(Message.class, "message")
.build();
生成的结果为:
/**
* Hides {@code message} from the caller's history. Other
* participants in the conversation will continue to see the
* message in their own history unless they also delete it.
*
* Use {@link #delete(Conversation)} to delete the entire
* conversation for all participants.
*/
void dismiss(Message message);
2.7 import 静态字段或方法
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards");
JavaFile.builder("com.example.helloworld", hello)
.addStaticImport(hoverboard, "createNimbus")
.addStaticImport(namedBoards, "*")
.addStaticImport(Collections.class, "*")
.build();
生成代码如下:
package com.example.helloworld;
import static com.mattel.Hoverboard.Boards.*;
import static com.mattel.Hoverboard.createNimbus;
import static java.util.Collections.*;
class HelloWorld {
}
AutoService注解无法生成META-INF文件?
在写注解处理器时,首先就是要继承AbstractProcessor,并且按照如下步骤声明:
- 需要在 processors 库的 main 目录下新建 resources 资源文件夹;
- 在 resources文件夹下建立 META-INF/services 目录文件夹;
- 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
- 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
这样声明下来也太麻烦了?这就是用引入auto-service的原因。
在类的顶部加入注解:@AutoService(Processor.class),这个注解处理器是Google开发的,可以用来生成 META-INF/services/javax.annotation.processing.Processor 文件信息。
使用遇到的问题
在module_processor中导入我们要用的auto-service库;
implementation 'com.google.auto.service:auto-service:1.0-rc6'
在类上面添加service的注解即可:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...}
编译项目后却始终不见META-INF目录的生成,正常是会在该注解处理器项目的目录module_processor/build/classes/java/main/META-INF下生成。
注意:可能是版本兼容问题,把版本降低一点
我们用到了AutoService, 使用@AutoService(Processor.class),编译后
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.[STATIC](https://so.csdn.net/so/search?q=STATIC&spm=1001.2101.3001.7020))
.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);
AutoService会自动在META-INF文件夹下生成Processor配置信息文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,
就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。