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

前言

很多时候系统处于安全考虑,将很多东西对外隐藏,而有时我们偏偏又不得不去使用这些隐藏的东西。甚至,我们希望向系统中注入一些自己的代码,修改原有代码的逻辑,以提高程序的灵活性,这时候就需要用到代码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语言,相信也是一样的,那么我们是怎么调用到Groovymain函数的呢?
在我们运行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其实就是调用到了GradlewWrapperMainmain方法
  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是怎么一步一步被加载的,其中要注意到BuildTreePreparingProjectsPreparerDefaultPluginManager两个步骤,分别承担构建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中存在一个与插件同名且包名也相同的类,就可以覆盖插件中的代码,从而达到修改原有代码逻辑的目的

参考资料

你的Gradle脚本究竟是什么
Gradle 庖丁解牛(构建源头源码浅析)

猜你喜欢

转载自juejin.im/post/7095511659925471240