AOP 中, 我们以处理阶段为划分产生了很多可选的技术手段:
- java 源代码阶段 (apt 、 ksp、 java)
- class 字节码阶段 (asm javaassist)
- dex 阶段
- 运行时阶段 (动态代理、cglib、javaassist)
其中 apt 处理的是 java 源代码文件, 操作起来比较简单, 是一项被广泛使用的技术
APT(Annotation Processing Tool) 技术是编译期间对注解的处理技术, 项目中若有很多类具有相似的样板代码, 可以考虑将这些样板代码在编译期间进行处理。
具体使用中, 常常会搭配 javapoet 来编译期间生成一些样板类, 解放手工
apt
apt 简单来说做的工作: 通过输入(java文件), 找到带有需要处理的注解的元素, 读取这些注解的信息, 为后续的 代码植入做准备。
apt 是 gradle build 阶段一个 task 触发的
正常执行下 app:assembleDebug 触发的 gradle task 如下:
Starting Gradle Daemon...
Gradle Daemon started in 1 s 286 ms
> Task :annotation:compileKotlin UP-TO-DATE
> Task :annotation:compileJava UP-TO-DATE
> Task :annotation:compileGroovy NO-SOURCE
> Task :annotation:processResources UP-TO-DATE
> Task :annotation:classes UP-TO-DATE
> Task :annotation:inspectClassesForKotlinIC UP-TO-DATE
> Task :annotation:jar UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
> Task :app:kaptDebugKotlin UP-TO-DATE
> Task :app:compileDebugKotlin UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
> Task :app:compileDebugSources UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:dexBuilderDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:packageDebug UP-TO-DATE
> Task :app:assembleDebug UP-TO-DATE
Task :app:kaptGenerateStubsDebugKotlin UP-TO-DATE
Task :app:kaptDebugKotlin UP-TO-DATE
就是 apt 的位置, apt 后才会生成 class 文件, 进一步dex , 最后 package
具体 apt 的代码都写在 AbstractProcessor 的实现类中
override fun init(processingEnvironment: ProcessingEnvironment?) {
super.init(processingEnvironment)
mTypeUtil = processingEnvironment?.getTypeUtils()
mElementUtil = processingEnvironment?.getElementUtils()
mFiler = processingEnvironment?.getFiler()
mMessager = processingEnvironment?.getMessager()
}
override fun process(
set: MutableSet<out TypeElement>,
processingEnvironment: RoundEnvironment
): Boolean {
// 具体 apt 代码
}
process 中, 可以根据 RoundEnvironment 可以取到所有带有某个注释的 类、接口、方法
一般流程:
整个过程来说:
- 解析注解
- 构造一个数据结构保存注解中的有效信息
javapoet
习惯语法即可
首先写一个 javaFile涉及到的核心步骤:
- 类相关 TypeSpec
- 构造函数 MethodSpec
- 成员变量 FieldSpec
- 方法 MethodSpec
- 注解 AnnotationSpec
具体如何使用可以直接参考:
https://blog.csdn.net/qq_17766199/article/details/112429217
不再赘述
val genClass =
TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
.addSuperinterface(ClassName.get(element))
.addModifiers(Modifier.PUBLIC)
for (field in fields) {
genClass.addField(field)
}
for (method in methods) {
genClass.addMethod(method)
}
JavaFile.builder(
mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
genClass.build()
)
.addFileComment("Generated code")
.build()
.writeTo(mFiler)
实践
- app 模块
- annotation模块
具体build.gradle 可以参考 github:
注解代码:
package com.example.perla
import com.example.annotation.*
@Man(name = "jackie", age = 1, coutry = JackCountry::class)
interface Jackie : IFigher {
@Body(weight = 200, height = 200)
fun body()
@GetCE(algorithm = Algorithm::class)
fun ce(): Int
@GetInstance
fun instance(): IFigher
}
class Algorithm : IAlgorithm {
override fun ce(figher: IFigher): Int {
return -1
}
}
class JackCountry : ICountry {
override fun name(): String {
return "China"
}
}
注解生成代码:
// Generated code
package com.example.perla;
import com.example.annotation.IAlgorithm;
import com.example.annotation.IFigher;
import java.lang.Override;
import java.lang.String;
import java.lang.System;
public class Jackie$$Impl implements Jackie {
private String mKey;
private String name;
private int age;
private String country;
private int weight;
private int height;
private IAlgorithm algorithm;
public Jackie$$Impl(String key) {
mKey = key;
name = "jackie";
age = 1;
country = new JackCountry().name();
algorithm = new Algorithm();
}
@Override
public void body() {
weight = 200;
height = 200;
}
@Override
public int ce() {
if (algorithm != null) {
return algorithm.ce(instance());
}
return weight + height;
}
@Override
public IFigher instance() {
return new Jackie$$Impl(String.valueOf(System.currentTimeMillis()));
}
}
核心代码:
PerlaProcessor.kt
package com.example.annotation
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreElements
import com.google.auto.service.AutoService
import com.squareup.javapoet.*
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedOptions()
@SupportedAnnotationTypes("*")
class PerlaProcessor : AbstractProcessor() {
private var mTypeUtil: Types? = null
private var mElementUtil: Elements? = null
private var mFiler: Filer? = null
private var mMessager: Messager? = null
private val aptSourceBook = HashMap<TypeElement, AptManInfo>()
override fun init(processingEnvironment: ProcessingEnvironment?) {
super.init(processingEnvironment)
mTypeUtil = processingEnvironment?.getTypeUtils()
mElementUtil = processingEnvironment?.getElementUtils()
mFiler = processingEnvironment?.getFiler()
mMessager = processingEnvironment?.getMessager()
}
override fun process(
set: MutableSet<out TypeElement>,
processingEnvironment: RoundEnvironment
): Boolean {
try {
for (element in processingEnvironment.getElementsAnnotatedWith(Man::class.java)) {
parseAnnotation(aptSourceBook, element as TypeElement)
}
write()
} catch (ex: Exception) {
}
return true
}
private fun write() {
for ((element, info) in aptSourceBook) {
val fields = ArrayList<FieldSpec>()
val methods = ArrayList<MethodSpec>()
val keyField = FieldSpec.builder(ClassName.get(String::class.java), "mKey")
.addModifiers(Modifier.PRIVATE).build()
val nameField = FieldSpec.builder(String::class.java, "name")
.addModifiers(Modifier.PRIVATE)
.build()
val ageField = FieldSpec.builder(Int::class.java, "age")
.addModifiers(Modifier.PRIVATE)
.build()
val countryField = FieldSpec.builder(String::class.java, "country")
.addModifiers(Modifier.PRIVATE)
.build()
val weightField = FieldSpec.builder(Int::class.java, "weight")
.addModifiers(Modifier.PRIVATE)
.build()
val heightField = FieldSpec.builder(Int::class.java, "height")
.addModifiers(Modifier.PRIVATE)
.build()
val algorithmField = FieldSpec.builder(IAlgorithm::class.java, "algorithm")
.addModifiers(Modifier.PRIVATE)
.build()
fields.add(keyField)
fields.add(nameField)
fields.add(ageField)
fields.add(countryField)
fields.add(weightField)
fields.add(heightField)
fields.add(algorithmField)
val constructor =
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(String::class.java), "key")
.addStatement("mKey = key")
.addStatement("name = \$S", info.name)
.addStatement("age = \$L", info.age)
.addStatement("country = new \$T().name()", info.country)
.addStatement("algorithm = new \$T()", info.algorithm)
val body =
MethodSpec.methodBuilder("body")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
info.bodyInfo?.let {
body.addStatement("weight = \$L", it.weight)
body.addStatement("height = \$L", it.height)
}
val ce =
MethodSpec.methodBuilder("ce")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.INT)
.beginControlFlow("if (algorithm != null)")
.addStatement("return algorithm.ce(instance())")
.endControlFlow()
.addStatement("return weight + height")
val getInstance =
MethodSpec.methodBuilder("instance")
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(IFigher::class.java))
.addStatement(
"return new \$T(String.valueOf(\$T.currentTimeMillis()))",
ClassName.bestGuess(element.simpleName.toString() + "$\$Impl"),
System::class.java
)
methods.add(constructor.build())
methods.add(body.build())
methods.add(ce.build())
methods.add(getInstance.build())
val genClass =
TypeSpec.classBuilder(element.simpleName.toString() + "$\$Impl")
.addSuperinterface(ClassName.get(element))
.addModifiers(Modifier.PUBLIC)
for (field in fields) {
genClass.addField(field)
}
for (method in methods) {
genClass.addMethod(method)
}
JavaFile.builder(
mElementUtil!!.getPackageOf(element).qualifiedName.toString(),
genClass.build()
)
.addFileComment("Generated code")
.build()
.writeTo(mFiler)
}
}
private fun parseAnnotation(
aptSourceBook: java.util.HashMap<TypeElement, AptManInfo>,
element: TypeElement
) {
val aptManInfo = AptManInfo()
val annotationInfo = element.getAnnotation(Man::class.java)
aptManInfo.apply {
name = annotationInfo.name
age = annotationInfo.age
country = getAnnotationClassName(element, Man::class.java, "coutry")?.toString()
?.let { ClassName.bestGuess(it) }
}
aptSourceBook[element] = aptManInfo
val methods = mElementUtil!!.getAllMembers(element)
.filter {
it.kind == ElementKind.METHOD &&
MoreElements.isAnnotationPresent(it, GetInstance::class.java) ||
MoreElements.isAnnotationPresent(it, GetCE::class.java) ||
MoreElements.isAnnotationPresent(
it,
Body::class.java
)
}.map { MoreElements.asExecutable(it) }.groupBy {
when {
MoreElements.isAnnotationPresent(it, Body::class.java) -> Body::class.java
MoreElements.isAnnotationPresent(
it,
GetInstance::class.java
) -> GetInstance::class.java
MoreElements.isAnnotationPresent(it, GetCE::class.java) -> GetCE::class.java
else -> Any::class.java
}
}
methods[Body::class.java]?.forEach {
val body = it.getAnnotation(Body::class.java)
aptManInfo.bodyInfo = BodyInfo().apply {
weight = body.weight
height = body.height
}
}
methods[GetInstance::class.java]?.forEach {
val instance = it.getAnnotation(GetInstance::class.java)
aptManInfo.getInstance = instance
}
methods[GetCE::class.java]?.forEach {
aptManInfo.algorithm =
getAnnotationClassName(it, GetCE::class.java, "algorithm").toString()
.let { ClassName.bestGuess(it) }
}
}
private fun getAnnotationClassName(
element: Element,
key1: Class<out Annotation>,
key: String
): Any? {
return MoreElements.getAnnotationMirror(element, key1)
.orNull()?.let {
AnnotationMirrors.getAnnotationValue(it, key)?.value
}
}
}