如何在Kotlin开发的Android Gradle插件中获取带@Test注解的测试方法
解决Gradle插件中扫描Android项目测试方法的问题
我之前开发Gradle插件时也踩过一模一样的坑——直接用反射根本找不到目标项目的类,因为插件运行在自己的类加载器上下文里,和你要分析的Android项目完全是两个独立的类空间。下面给你几个靠谱的解决思路,按推荐程度排序:
1. 直接解析源码文件(最推荐)
不需要依赖编译后的类,用源码解析库直接读取Java/Kotlin源码的AST(抽象语法树),找出带@Test注解的方法。JavaParser是个很好用的工具,支持Java和Kotlin(Kotlin可以用kotlin-parsers扩展)。
步骤:
- 给你的Gradle插件项目添加JavaParser依赖:
dependencies { implementation("com.github.javaparser:javaparser-core:3.25.8") // 如果要处理Kotlin源码 implementation("com.github.javaparser:kotlin-parsers:3.25.8") } - 遍历你拿到的
ArrayList<File>,逐个解析文件:
这里要注意区分JUnit 4和JUnit 5的@Test注解全类名,根据你的项目实际情况调整判断逻辑。import com.github.javaparser.JavaParser import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.body.MethodDeclaration import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations fun scanTestMethodsFromSourceFiles(testFiles: List<File>) { val javaParser = JavaParser() testFiles.forEach { file -> val cu: CompilationUnit = if (file.extension == "kt") { // 处理Kotlin文件 com.github.javaparser.kotlin.KotlinParser().parse(file) } else { javaParser.parse(file) } // 遍历所有类中的方法 cu.findAll(MethodDeclaration::class.java).forEach { method -> // 检查是否带有@Test注解(注意区分JUnit 4和JUnit 5的全类名) val hasTestAnnotation = method.isAnnotationPresent("org.junit.Test") || method.isAnnotationPresent("org.junit.jupiter.api.Test") if (hasTestAnnotation) { println("找到测试方法:${method.nameAsString} 在文件 ${file.name}") } } } }
2. 解析编译后的字节码
如果你的插件是在测试编译任务之后运行,可以直接读取编译好的字节码文件,用ASM或者ByteBuddy这样的字节码操作库来扫描注解,这种方式比解析源码更准确(能处理编译时生成的代码)。
步骤:
- 先获取Android项目测试编译任务的输出目录,比如
debugTestClasses:val androidExtension = project.extensions.getByName("android") as com.android.build.gradle.api.AndroidBasePluginExtension val testVariant = androidExtension.testVariants.find { it.name == "debug" } val testClassesDir = testVariant?.compileProvider?.get()?.outputs?.files?.firstOrNull() - 用ASM扫描字节码中的@Test注解:
import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.Opcodes import java.io.FileInputStream fun scanTestMethodsFromBytecode(classFiles: List<File>) { classFiles.forEach { file -> FileInputStream(file).use { inputStream -> val classReader = ClassReader(inputStream) classReader.accept(object : ClassVisitor(Opcodes.ASM9) { override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): org.objectweb.asm.MethodVisitor? { // 检查方法是否带有@Test注解 return object : org.objectweb.asm.MethodVisitor(Opcodes.ASM9) { override fun visitAnnotation(desc: String?, visible: Boolean): org.objectweb.asm.AnnotationVisitor? { // 匹配JUnit 4或JUnit 5的@Test注解的内部名称 if (desc == "Lorg/junit/Test;" || desc == "Lorg/junit/jupiter/api/Test;") { println("找到测试方法:$name 在类文件 ${file.name}") } return super.visitAnnotation(desc, visible) } } } }, ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) } } }
3. 动态创建目标项目的类加载器(不推荐)
如果一定要用反射,你需要构建一个包含目标项目测试类和所有依赖的类加载器,然后加载类进行反射。但这种方式非常复杂,因为Android项目的依赖树很庞大,还要处理不同变体的classpath,容易出问题。这里给个简化的示例:
import java.net.URLClassLoader fun createProjectClassLoader(project: Project, testClassesDir: File): ClassLoader { // 收集测试类的classpath,包括项目编译后的类和所有测试依赖 val classpath = mutableListOf<File>() classpath.add(testClassesDir) // 添加测试依赖的JAR包 project.configurations.getByName("debugTestImplementation").files.forEach { classpath.add(it) } project.configurations.getByName("debugTestRuntimeOnly").files.forEach { classpath.add(it) } val urls = classpath.map { it.toURI().toURL() }.toTypedArray() return URLClassLoader(urls, null) // 用null作为父加载器,避免和插件的类加载器冲突 } // 然后用这个类加载器加载类并反射 fun scanTestMethodsViaReflection(testFiles: List<File>, classLoader: ClassLoader, project: Project) { testFiles.forEach { file -> // 从文件路径转换为类全限定名,比如com/example/TestClass.java -> com.example.TestClass val className = file.absolutePath .replace("${project.projectDir.absolutePath}/src/test/java/", "") .replace(".java", "") .replace(".kt", "") .replace("/", ".") try { val clazz = classLoader.loadClass(className) clazz.declaredMethods.forEach { method -> if (method.isAnnotationPresent(org.junit.Test::class.java) || method.isAnnotationPresent(org.junit.jupiter.api.Test::class.java)) { println("找到测试方法:${method.name}") } } } catch (e: ClassNotFoundException) { // 处理类找不到的异常,可能是路径转换错误或者依赖缺失 e.printStackTrace() } } }
这种方式需要你准确处理类路径和类名转换,而且如果项目用了Kotlin的一些特性(比如默认方法、内联函数),反射可能会有奇怪的问题,所以除非必要,不建议用。
总结一下,优先用源码解析或者字节码解析的方式,这两种都能避开类加载器的问题,而且更稳定。
内容的提问来源于stack exchange,提问作者rdiaz82
相关产品推荐
相关产品推荐

