如何简单方便地Hook Gradle插件?
作者:程序员江同学
转载地址:https://juejin.cn/post/7095511659925471240
前言
很多时候系统处于安全考虑,将很多东西对外隐藏,而有时我们偏偏又不得不去使用这些隐藏的东西。甚至,我们希望向系统中注入一些自己的代码,修改原有代码的逻辑,以提高程序的灵活性,这时候就需要用到代码Hook。
在Java或者Kotlin代码中,代码Hook有多种方案,比如反射,动态代理,或者通过修改字节码来实现HOOK,那么如果我们想要修改Gradle插件的代码,该怎么实现呢?
简单使用
我们首先来看一个简单的例子,大家肯定都用过com.android.application插件,如果我们想要在这个插件中添加一些代码,可以怎么操作呢?修改方式非常简单
- 项目中添加buildSrc模块
- buildSrc中添加com.android.tools.build:gradle:7.0.2依赖
- 在buildSrc中添加与插件中同名的AppPlugin即可,如下所示
package com.android.build.gradle
import org.gradle.api.Project
class AppPlugin: BasePlugin() {
override fun apply(project: Project) {
super.apply(project)
println("hook AppPlugin demo")
project.apply(INTERNAL_PLUGIN_ID)
}
}
private val INTERNAL_PLUGIN_ID = mapOf("plugin" to "com.android.internal.application")
然后我们再同步一下项目,就可以发现hook AppPlugin demo的日志可以打印出来了,就这样在AppPlugin中添加了我们想要的逻辑
在了解怎么使用了之后,我们再来分析下为什么这样做就可以覆盖插件中的AppPlugin,我们首先需要了解下Gradle插件到底是怎么运行起来的
Gradle运行的入口是什么?
我们都知道,Java运行需要一个main函数,Groovy作为一个JVM语言,相信也是一样的,那么我们是怎么调用到Groovy的main函数的呢?
在我们运行Gradle的时候,都是通过gradlew来运行的,gradlew其实是对gradle的一个包装,本质上就是一个shell脚本
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
可以看出,其实就是调用了GradleWrapperMain并传递给它一系列参数,那我们再来看下GradleWrapperMain
public class GradleWrapperMain {
......
//执行 gradlew 脚本命令时触发调用的入口。
public static void main(String[] args) throws Exception {
......
//调用BootstrapMainStarter
wrapperExecutor.execute(
args,
new Install(logger, new Download(logger, "gradlew", wrapperVersion()), new PathAssembler(gradleUserHome)),
new BootstrapMainStarter());
}
}
public class BootstrapMainStarter {
public void start(String[] args, File gradleHome) throws Exception {
//调用GradleMain的main方法
Class<?> mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain");
Method mainMethod = mainClass.getMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
......
}
可以看出
- gradlew其实就是调用到了GradlewWrapperMain的main方法
- 然后再通过BootstrapMainStarter方法调用到GradleMain,这里才是Gradle执行真正的入口
当前插件是怎样调用的?
上面介绍了Gradle运行了的入口,但是要从入口跟代码跟到我们插件加载的入口是非常麻烦的,我们换个思路,看下AppPlugin是怎么被加载的
class AppPlugin: BasePlugin() {
override fun apply(project: Project) {
//...
RuntimeException().printStackTrace()
}
}
我们在加载AppPlugin时通过以下方式直接打印出堆栈即可,堆栈如下所示:
java.lang.RuntimeException
at com.android.build.gradle.AppPlugin.apply(AppPlugin.kt:9)
at com.android.build.gradle.AppPlugin.apply(AppPlugin.kt:5)
at org.gradle.api.internal.plugins.ImperativeOnlyPluginTarget.applyImperative(ImperativeOnlyPluginTarget.java:43)
...
at org.gradle.configuration.internal.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:43)
at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:156)
at org.gradle.api.internal.plugins.DefaultPluginManager.apply(DefaultPluginManager.java:127)
...
at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:64)
at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52)
...
通过这些堆栈,我们就可以看出AppPlugin是怎么一步一步被加载的,其中要注意到BuildTreePreparingProjectsPreparer和DefaultPluginManager两个步骤,分别承担构建classloader父子关系与设置当前线程上下文classloader,感兴趣的同学可以直接查看源码
Gradle类加载机制
我们通过在buildSrc中添加同名类的方式就可以实现覆盖插件中代码的效果,猜想应该是通过类似Java的类加载机制实现,我们首先打印下app模块的classLoader
fun printClassloader(){
println("classloader:"+this.javaClass.classLoader)
println("classloader parent:"+this.javaClass.classLoader.parent)
println("classloader grantparent:"+this.javaClass.classLoader.parent.parent)
}
如上,分别打印classloader与父祖classloader,输出结果如下
classloader:VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:settings[:buildSrc]:buildSrc[:buildSrc]:root-project[:buildSrc]:Project/TopLevel/stage2(local)})
classloader parent:VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:settings[:buildSrc]:buildSrc[:buildSrc]:root-project[:buildSrc](export)})
classloader grantparent:CachingClassLoader(FilteringClassLoader(VisitableURLClassLoader(legacy-mixin-loader)))
可以看出,其实buildSrc模块的classloader其实是当前模块的父classLoader,在双亲委托机制下,会首先委托给父classloader来查找,那么在buildSrc模块中已经加载了的类自然会覆盖插件中的类了,也就可以轻松实现对插件代码逻辑的修改
总结
由于在Gradle代码运行过程中,buildSrc模块的classloader是项目中module的父classloader,因此在加载类的过程中,会首先委托给父classloader来查找,如果我们在buildSrc中存在一个与插件同名且包名也相同的类,就可以覆盖插件中的代码,从而达到修改原有代码逻辑的目的