利用Javassist编译期创建注解类(Annotation Class)

一、需求:某Gradle插件被apply的时候给当前project添加一个注解类

Gradle插件用户使用该注解类标记需要在编译期被处理的类,目标注解类内容如下:

package com.test.javassist;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface JavassistAnnotation {
    String value();
}
复制代码

二、实现:利用Javassist生成目标注解类

编译期生成类的方案有很多:javapoet可生成未编译的java文件,javassist可生成编译完的class文件,由于该Gradle插件已经使用javassist来做后续字节码的处理,故选择使用javassist来生成class,具体实现(kotlin版)如下:

  1. 创建目标注解类 -> public @interface JavassistAnnotation
val classPool = ClassPool.getDefault()
// className:包名+类名的完整路径
val className = "com.test.javassist.JavassistAnnotation"
val ctClass = classPool.makeAnnotation(className)
复制代码
  1. 给目标类添加方法-> String value();
// 方法中的返回值String需要提供java空间的完整类名,"value"是方法签名
val ctMethod = CtMethod(classPool.get(String::class.java.name), "value", null, ctClass)
ctClass.addMethod(ctMethod)
复制代码
  1. 给目标类添加元注解-> @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS)
// 先创建常量池
val cFile = ctClass.classFile
val cPool = cFile.constPool
val annotationAttributes = AnnotationsAttribute(cPool, AnnotationsAttribute.visibleTag)

// 设置元注解@Target
val targetAnnotation = Annotation("java.lang.annotation.Target", cPool)
// @Target的方法签名是"value" 返回值是枚举类型ElementType
// 故使用EnumMemberValue设置值,其他类型的返回值请选择相对应的MemberValue类
targetAnnotation.addMemberValue("value", EnumMemberValue(cPool).apply {
    // 设置@Target方法的返回值类型
    type = "java.lang.annotation.ElementType"
    // 设置@Target方法的返回值具体对应的值
    value = "TYPE"
})

// 设置元注解@Retention
val retainAnnotation = Annotation("java.lang.annotation.Retention", cPool)
// @Retention的方法签名是"value" 返回值是枚举类型RetentionPolicy
// 故使用EnumMemberValue设置值,其他类型的返回值请选择相对应的MemberValue类
retainAnnotation.addMemberValue("value", EnumMemberValue(cPool).apply {
    // 设置@Retention方法的返回值类型
    type = "java.lang.annotation.RetentionPolicy"
    // 设置@Retention方法的返回值具体对应的值
    value = "CLASS"
})

// 添加设置好的元注解
annotationAttributes.addAnnotation(retainAnnotation)
annotationAttributes.addAnnotation(targetAnnotation)
cFile.addAttribute(annotationAttributes)
复制代码
  1. 生成目标类
// 注意类文件的输出路径,如果不填直接写到当前project的根目录(如:app)
ctClass.writeFile(outputDirPath)
// 释放内存
ctClass.detach()
复制代码

三、附加:编译期生成的类文件该放在什么位置

由于javassist生成的是已经编译完的class文件,故而不能像使用javapoet生成的java文件一样放在generate目录下,class文件需要通过被依赖的方式导入项目才能使用。

如何导入呢?该libs文件夹登场了。

实现思路:

  1. 用javassist把目标文件输出到libs中的临时目录tmpDir
  2. 把tmpDir压缩成jar文件destFile.jar,并删除tmpDir
  3. 项目repositories中添加libs目录
  4. 项目dependencies中添加destFile.jar依赖
val libsDir = "${buildDir.parent}/libs"
val tmpDir = File(libsDir, "tmp")
val destFile = File(libsDir, "javassistAnnotation.jar")
JarUtil.jarFile(tmpDir, destFile)
tmpDir.deleteRecursively()

repositories.flatDir {
    it.dir(libsDir)
}
dependencies.add(
    "implementation",
    mapOf("name" to destFile.nameWithoutExtension, "ext" to destFile.extension)
)
复制代码

四、最后:其他方案

至此,我们已经成功生成目标注解类并导入项目中。但是,洁癖者吐槽说:通过此种方式给项目添加类,由于在libs目录下生成了新的jar包,在as默认设置的情况下,cvs会提示添加了新文件,是否加入到cvs中。还有其他所谓更优雅的方案吗?

  1. 使用javapoet生成java类,可以在BuildConfig的task后直接输出到build/generate目录。这样不需要打成jar包,也无需添加依赖,同时还可以避免cvs提示,用户无感知。
  2. 新建一个java项目,置入目标类,把该项目发布到maven仓库,直接在apply插件的时候引入依赖即可。这样不需要生成类(体力换脑力),通过url添加依赖同样可以避免cvs的提示,用户无感知。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享