前言

很多时候系统处于安全考虑,将很多东西对外隐藏,而有时我们偏偏又不得不去使用这些隐藏的东西。甚至,我们希望向系统中注入一些自己的代码,修改原有代码的逻辑,以提高程序的灵活性,这时候就需要用到代码​​Hook​​​。
在​​​Java​​​或者​​Kotlin​​​代码中,代码​​Hook​​​有多种方案,比如反射,动态代理,或者通过修改字节码来实现​​HOOK​​​,那么如果我们想要修改​​Gradle​​插件的代码,该怎么实现呢?

简单使用

我们首先来看一个简单的例子,大家肯定都用过​​com.android.application​​插件,如果我们想要在这个插件中添加一些代码,可以怎么操作呢?修改方式非常简单

  1. 项目中添加​​buildSrc​​模块
  2. ​buildSrc​​​中添加​​com.android.tools.build:gradle:7.0.2​​依赖
  3. 在​​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});
}
......
}

可以看出

  1. ​gradlew​​​其实就是调用到了​​GradlewWrapperMain​​的​​main​​方法
  2. 然后再通过​​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​​中存在一个与插件同名且包名也相同的类,就可以覆盖插件中的代码,从而达到修改原有代码逻辑的目的



如何简单方便地Hook Gradle插件?_java