gradle实践-build.gradle解析原理

Posted 陌路风尘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gradle实践-build.gradle解析原理相关的知识,希望对你有一定的参考价值。

前言

基于gradle4.8源码,每个版本会有改动但是大体的设计理念还是不变的

本文主要通过源码解读gradle.build文件的解析过程,以及gradle 部分源码的设计思路。其中有些内容可能并不严谨,不正确的,随时可以指出,谢谢~~

groovy的MOP和闭包

在解析gradle配置文件运行机制前,我先简单的介绍下groovy的MOP(MetaObject Protocol元对象)基本概念,如果需要深入了解的可以去groovy官网自行学习。

GroovyObject

GroovyObject是所有grovvy对象的基类

 
   
   
 
  1. public interface GroovyObject {

  2.    Object invokeMethod(String name, Object args);

  3.    Object getProperty(String propertyName);

  4.    void setProperty(String propertyName, Object newValue);

  5.    MetaClass getMetaClass();

  6.    void setMetaClass(MetaClass metaClass);

  7. }

对于自定义的对象如果实现 GroovyObjectinvokeMethod, get/setProperty方法,那么属性引用和方法调用都会通过自己的实现进行拦截。下面的例子可以很好的解释了其运行机制。

 
   
   
 
  1. class Person implements GroovyInterceptable {

  2.    //GroovyInterceptable接口仅仅是一个标示,代表无论此对象是否有定义好的方法,

  3.    //都会调用invokeMethod方法,优先拦截作用

  4.    def name

  5.    def mapForStoreUnKnowProperty = new HashMap()//存储动态属性

  6.    Person(name) {

  7.        this.name = name

  8.    }


  9.    @Override

  10.    Object getProperty(String s) {

  11.        System.out.println "getProperty $s"

  12.        if (s == "name") {

  13.            return this.name

  14.        }

  15.        //如果动态属性也没有,那么就返回unknow

  16.        if (mapForStoreUnKnowProperty.containsKey(s)) {

  17.            mapForStoreUnKnowProperty.get(s)

  18.        } else {

  19.            "unknow property $s"

  20.        }


  21.    }

  22.    //如果没有此属性,那么使用动态属性容器进行存储。

  23.    @Override

  24.    void setProperty(String s, Object o) {

  25.        System.out.println "setProperty $s"

  26.        if (s == "name") {

  27.            this.name = o

  28.        } else {

  29.            mapForStoreUnKnowProperty.put(s, o)

  30.        }

  31.    }


  32.    //拦截方法

  33.    @Override

  34.    Object invokeMethod(String s, Object o) {

  35.        System.out.println "invokeMethod $s $o"

  36.        if (s == "sayHelloWord") {

  37.            System.out.println "hello word"

  38.        } else {

  39.            System.out.println "no method"

  40.        }

  41.    }


  42.    static void main(String[] args) {

  43.        def person = new Person("god")

  44.        System.out.println "person ${person.name} ${person.noProperty}"

  45.        person.noProperty = "动态属性value"

  46.        System.out.println "person  ${person.noProperty}"

  47.        person.sayHelloWord()

  48.    }

  49. }


  50. 输出内容:

  51. getProperty name

  52. getProperty noProperty

  53. person god unknow property noProperty

  54. setProperty noProperty

  55. getProperty noProperty

  56. person  动态属性value

  57. invokeMethod sayHelloWord []

  58. hello word

MetaClass

groovy对象除了自身可以重写代理拦截方法和属性外,还可以通过设置MetaClass的方式进行属性和方法的拦截。metaClass的定义如下:

 
   
   
 
  1. public interface MetaClass extends MetaObjectProtocol {

  2.    //拦截方法

  3.    Object invokeMethod(Class var1, Object var2, String var3, Object[] var4, boolean var5, boolean var6);

  4.    //拦截属性

  5.    Object getProperty(Class var1, Object var2, String var3, boolean var4, boolean var5);

  6.    void setProperty(Class var1, Object var2, String var3, Object var4, boolean var5, boolean var6);

  7.    //如果没有重写invokeMethod同时又没有定义此方法,会回调到此处

  8.    Object invokeMissingMethod(Object var1, String var2, Object[] var3);

  9.    ....//省略一些~~

  10. }

所有的groovy对象,可以调用setMetaClass是进行设置元对象,具体的例子就不提供了。

metaClass的属性和方法拦截优先级高于自身实现。

Groovy闭包Closure

Groovy对于用大括号包起来的代码块会生成一个Closure闭包对象,闭包有两个对象 Delegateowner对象,对于代码块的中属性和方法调用,首先会委托owner处理,如果未找到,则会使用deletegate处理,同时闭包支持动态修改这两个对象,这也是Groovy的语言特点,也是groovyDSL的基础,更加详细关于闭包的内容可以看groovy的官网。

gradle配置脚本评估(evaluate)

gradle的ASM

gradle使用java和groovy语言共同构建,同时使用了ASM(字节码操作)对很多内部类进行了改造,比如:为属性增加get或者set方法,为类增加接口和实现,增加DSL特性等,所以在解析源码的时候可能会遇到接口判断,方法不存在等,但是却又能正常执行的情况。凡是在调试的时候类名的描述加上了 __Decorated都是改造后的类.

有几个比较重要的改造内容:

  1. 为类增加DynamicObjectAware接口,返回DynamicObject对象。这个操作非常的普遍,很多类都增加了这个实现,然后调用getAsDynamicObject返回与之关联的dynamicObject。

  2. 很多方法的参数是Action,gradle会默认生成一个调用action的Closure的方法。

  3. 为java类增加metaclassdynobj等属性,并提供get方法。

build.gradle解析入口

gradle的执行是通过groovy进行脚本解析和运行,每一个buid.gradle都会生成一个对应的Project对象,这个project对象就是这个脚本文件的执行环境。(那对于这个对象产生,执行环境的生成是很复杂的过程,并且使用了ASM,有兴趣的可以自行解读)

直观上看,就好比build.gradle是一个闭包,而 BaseScript是这个闭包的delegate对象。

DynamicObject 动态对象

上图中的DynamicObject,是gradle源码中非常重要的组成部分,主要负责脚本属性和方法的委托,它有几个重要的实现类,使用了装饰着模式进行代理。下图是DynamicObject的类图关系

由于截图比较小,可以查看我的原始图

ConfigureDelegate 配置代理

配置代理对象是对DynamicObject执行的封装,具体的可以看源代码,比较简单,它有个常用的子类 NamedDomainObjectContainerConfigureDelegate,这个子类扩展了动态创建dsl,函数的能力,主要于 NamedDomainObjectContainer配合使用(后面会详细介绍 NamedDomainObjectContainer

ConfigureDelegate还有个工具类 ConfigureUtil,提供便捷的ConfigureDelegate对象构造,执行的函数。

评估脚本配置

为了更加方便的解读,在github上创建了个gradle的项目configuration,后面分析全都是以该项目的build.gradle为基础的,build.gradle文件比较长, 就不贴出来了。同时为了方便理解,我把此文件划分了几个部分,后面会一一说明。

脚本解读的代码入口是 BasicScriptinvokeMethod/getProperty/setProperty方法,它则会直接调用 ScriptDynamicObject的相关方法,而 ScriptDynamicObject中有个成员变量 dynamicTarget一般此 DynamicObjectExtensibleDynamicObject对象,所以只需要在 ScriptDynamicObject相关方法打上断点,就可以拦截到执行的过程。

在project中的extensibleDynamicObject对象和上面的ExtensibleDynamicObject是同一个对象,而project的属性和方法查找也会执行到ExtensibleDynamicObject中,应该是project的metaClass使用project的extensibleDynamicObject对象进行操作。所以在脚本里project.name和name执行的效果是一样的。project有个getProject方法,返回了自己。

ExtensibleDynamicObject中有个很重要的容器 objectDynamicObject[],它一般含有四个对象

(版本不同插件不同可能会有所不同,但是原理是一样的,我这里使用了最基础的java插件)

  1. BeanDynamicObject此对象方法和属性的查找委托给了自身代理的Bean也就是project对象,bean通过MetaClass进行查找。

  2. ExtraPropertieDynamicObjectAdapter只能读取/修改属性,不能代理方法,此处代理的对象其实是 DefaultConvention中的 extensionsStorage里的 ext,其实应该是为了加快属性读取和修改效率的。因为这两个对象是同一个。

  3. DefaultConvention.ExtensionsDynamicObject。 DefaultConvention负责管理插件提供的约束(DSL配置),同时还负责 extensionsStorage中扩展配置,例如: ext的扩展属性, reporting的报告配置等。

  4. DefaultTaskContainer(DefaultNamedDomainObjectCollection).ContainerElementsDynamicObject 存储所有task信息,支持task的属性访问和方法调用

ok,现在了解基本的配置情况后,看一下实际的运行情况。

第一部分 project属性配置

 
   
   
 
  1. plugins {

  2.    id 'java'  //java插件

  3.    id 'com.demon.yu'  //自定义的插件

  4. }//不做分析

  5. /**第一部分*/

  6. //defaultProject中的方法

  7. group 'com.demon.yu.gradle'

  8. project.version '1.0-SNAPSHOT'

  9. //defaultProject属性

  10. rootProject.version = "1.0-SNAPSHOT"

看下 group'com.demon.yu.gradle'这行脚本,会被BaseScript解析成方法调用,方法名称为 group参数为 'com.demon.yu.gradle'字符串。然后BaseScript对象,会通过dyanamicObject对象调用到 CompositeDynamicObjecttryGetMethod方法中, CompositeDynamicObjecttryInvokeMethod方法实现如下:

 
   
   
 
  1.    @Override

  2.    public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {

  3.        for (DynamicObject object : objects) {

  4.            DynamicInvokeResult result = object.tryInvokeMethod(name, arguments);

  5.            if (result.isFound()) {

  6.                return result;

  7.            }

  8.        }

  9.        return DynamicInvokeResult.notFound();

  10.    }

遍历我们前面所说的容器,如果某个 dynamicObject找到对应的方法就处理,如果没有找到则返回。 对于 group'com.demon.yu.gradle'脚本块来说,由于 defaultProjectmetaClass有group(String),其内部调用了defaultProject.setGroup(String)方法,所以在容器第一个beanDynamicObject中就找到并反射调用。这样此代码块就执行了。

前面说过defaultProject经过了ASM改造,所以有个metaClass属性和getMetaClass方法,而且对于group属性也生成了group(String) 方法。

对于 project.version'1.0-SNAPSHOT'rootProject.version="1.0-SNAPSHOT"执行的过程类似,只不过是先找到 DefaultProjectprojectrootProject属性,然后通过metaClass,DefaultProject调用setVertion(String)方法或者直接给属性vertion赋值

此处对于直接赋值比较好理解,但是对于调用setVertion可能有些疑惑,其实这里在找到project属性后,后面跟着名字+空格+参数,那么脚本解释器会把这些组成CallSite调用点执行对象,直接通过project的metaClass查找方法并反射调用。

第二部分 ext属性配置

第二部分主要解析扩展属性变量

 
   
   
 
  1. //ext是DefaultExtraPropertiesExtension

  2. ext.vertionCode = 1

  3. ext {//闭包环境为DefaultExtraPropertiesExtension

  4.    targetSDK = 28

  5. }

首先来看 ext.vertionCode=1,和第一部分一样,首先查找ext属性,在容器第三个对象 DefaultConvention.ExtensionsDynamicObject中,通过源码可以看到,它并不是查找自身属性,而是通过两个容器对象 extensionsStroageplugins进行查找,看下源码:

 
   
   
 
  1. @Override

  2. public DynamicInvokeResult tryGetProperty(String name) {

  3.    Object extension = extensionsStorage.findByName(name);

  4.    if (extension != null) {

  5.        return DynamicInvokeResult.found(extension);

  6.    }

  7.    ...//省略其他的

  8.    return DynamicInvokeResult.notFound();

  9. }

extensionsStorage是个工具类,封装了一个 LinkedHashMap<String,Object>容器,用来存储键值对象。在DefaultConvention初始化的时候,默认添加了个 ext:String->extraProperties:DefaultExtraPropertiesExtension数据,所以gradle默认就可以处理ext DSL的数据和闭包,javaBasePlugin和javaPlugin等等插件也会通过 DefaultConventionextensionsStorage中添加数据。除了 extensionsStorageDefaultConvention还会查询plugins,plugins存放的是插件的Convention对象后面第三部分会详细讲解。在找到ext里的 ExtraPropertiesExtension对象后,函数返回,然后开始处理 DefaultExtraPropertiesExtension.vertionCode=1的脚本。

DefaultExtraPropertiesExtension实现了 GroovyObjectSupport重写了get/setProperty方法,所以属性调用会执行get/setProperty方法,这样DefaultExtraPropertiesExtension会存储相应键值对数据了。 对于代码段

 
   
   
 
  1. ext {

  2.    targetSDK = 28

  3. }

执行原理类似,不过此处会执行 DefaultConventiontryInvokeMethod方法,因为脚本解释器解析的结果是方法调用 - ext(Closure).

 
   
   
 
  1. @Override

  2. public DynamicInvokeResult tryInvokeMethod(String name, Object... args) {

  3.    if (isConfigureExtensionMethod(name, args)) {   //判断包含ext同时第1个参数是闭包,那么就会执行configureExtension(name, args)

  4.        return DynamicInvokeResult.found(configureExtension(name, args));

  5.    }

  6.    ....

  7.    return DynamicInvokeResult.notFound();

  8. }

  9. private boolean isConfigureExtensionMethod(String name, Object[] args) {

  10.    return args.length == 1 && args[0] instanceof Closure && extensionsStorage.hasExtension(name);

  11. }

configureExtension(name,args)方法的最终结果是以 DefaultExtraPropertiesExtension为deletegate执行闭包。最后把闭包的数据添加到 DefaultExtraPropertiesExtension中数据容器里。此时的结果 DefaultExtraPropertiesExtension存储了两个数据 vertionCode=1targetSDK=28,这个时候会发现在ExtensibleDynamicObject中的第二个dynamiobject(ExtraPropertiesDynamicObjectAdapter)中的 ExtraPropertiesExtension也会直接包含了两个数据,所以后面的脚本可以直接读取/修改数据,但是不可以设置新的数据。

 
   
   
 
  1. public class ExtraPropertiesDynamicObjectAdapter extends AbstractDynamicObject {

  2.    private final ExtraPropertiesExtension extension;//和defaultConvention中extensionsStorage存储的ext对应对应的DefaultExtraPropertiesExtension是同一个对象。

  3.    public ExtraPropertiesDynamicObjectAdapter(Class<?> delegateType, ExtraPropertiesExtension extension) {

  4.        this.delegateType = delegateType;

  5.        this.extension = extension;

  6.    }

  7.    ...//省略其他的

  8.    @Override

  9.    public DynamicInvokeResult tryGetProperty(String name) {

  10.        if (extension.has(name)) {

  11.            return DynamicInvokeResult.found(extension.get(name));

  12.        }

  13.        return DynamicInvokeResult.notFound();

  14.    }

  15.    @Override

  16.    public DynamicInvokeResult trySetProperty(String name, Object value) {

  17.        if (extension.has(name)) {

  18.            extension.set(name, value);

  19.            return DynamicInvokeResult.found();

  20.        }

  21.        return DynamicInvokeResult.notFound();

  22.    }

  23.    ...//省略其他

  24. }

第三部分 DefaultConcention属性配置

这个部分主要解析DefaultConvention相关内容

 
   
   
 
  1. /**第三部分*/

  2. //javaPluginConvention

  3. sourceCompatibility = 1.8

  4. //javaPluginConvention //执行的context

  5. sourceSets {

  6.    //sourceSetContainer

  7.    main {

  8.        //sourceSet

  9.        java {

  10.            //sourceDirectorySet

  11.            srcDirs = ["src/java"]

  12.            exclude 'some/unwanted/package/**'

  13.        }

  14.    }

  15.    namain{java{}}//可以随便写的

  16. }

首先看fu下 sourceCompatibility=1.8,还是老套路,ExtensibleDynamicObject容器中第三个DynamicObject(ExtensionsDynamicObject)找到结果。看一下它的实现。

 
   
   
 
  1. @Override

  2. public DynamicInvokeResult trySetProperty(String name, Object value) {

  3.    checkExtensionIsNotReassigned(name);

  4.    if (plugins == null) {

  5.        return DynamicInvokeResult.notFound();

  6.    }

  7.    for (Object object : plugins.values()) {

  8.        BeanDynamicObject dynamicObject = asDynamicObject(object).withNotImplementsMissing();//以Convention对象封装一个BeanDynamicObject实例,然后执行属性设值。

  9.        DynamicInvokeResult result = dynamicObject.trySetProperty(name, value);

  10.        if (result.isFound()) {

  11.            return result;

  12.        }

  13.    }

  14.    return DynamicInvokeResult.notFound();

  15. }

plugins存储的是插件添加的Convention实例,例如:java插件的JavaPluginConvention。在JavaPluginConvention中找到sourceCompatibility属性并赋值(这个地方有点特殊,找到的是MultipleSetterProperty属性,需要调用其关联的set方法进行赋值的)

再来看 sourceSets{闭包}部分,同样在JavaPluginConvention中找到sourceSets(Closure)方法,其内部实现直接调用 DefaultSourceSetContainer.configure(Closure)DefaultSourceSetContainerNamedDomainObjectContainer的子类, NamedDomainObjectContainer是键值对容器,方法调用(只有一个闭包参数的方法)和属性读取/赋值都是查找是否存在对应键名,它和 NamedDomainObjectContainerConfigureDelegate形成一个动态创建domain的功能,调用关系和组织结构如下:

在找到 DefaultSourceSetContainer后,继续执行main{闭包},此时在 DefaultSourceSetContainer中找到名字为main的sourceSet,然后就继续上面类似的操作。

JavaPlugin在DefaultSourceSetContainer添加了main和test两个DefaultSourceSet,DefaultSourceSet默认设置了源码文件夹和资源文件夹等数据

第四部分 task定义配置相关

task是gradle非常重要的部分,task的定义和使用主要涉及到ExtensibleDynamicObject容器中 BeanDynamicObjectDefaultNamedDomainObjectCollection.ContainerElementsDynamicObject

 
   
   
 
  1. //直接反射到defaultProject的task方法

  2. task helloWord(dependsOn: 'jar') {

  3.    //DefaultTask

  4.    println("配置task helloWord时执行")

  5.    group="groupName"

  6.    description = "任务描述"

  7.    doLast {

  8.        println("执行helloWord doLast")

  9.    }

  10. }

  11. println helloWord.description


  12. //很多种定义task的写法,具体可以看下DefaultProject的实现

  13. def map = ["dependsOn": 'helloword']

  14. task map, "helloNext", {

  15. }

  16. task noThing

task函数,都位于 DefaultProject中,基本上直接调用成员变量 DefaultTaskContainer的create和configure方法。 DefaultTaskContainer同样是 NamedDomainObjectContainer子类,主要负责创建存储Task,同时他的祖先类中的变量D efaultNamedDomainObjectCollection.ContainerElementsDynamicObject负责查找和执行Task。task定义过程和第三部分过程差不多。唯一的不同 helloWord.description这种任务读取使用的是 DefaultNamedDomainObjectCollection.ContainerElementsDynamicObject委托。

第五部分 其他

第五部分简单说下gradle的configuration

 
   
   
 
  1. /**第五部分*/

  2. //configurationContainer是NamedDomainObjectContainer。sourceSetContainer类似的东西

  3. configurations {

  4.    myConfiguration

  5.    compile.transitive = true

  6. }


  7. //defaultProject

  8. artifacts { //DefaultArtifactHandler

  9.    myConfiguration jar   //jar是task名字,根据源码还可以支持很多种类型,

  10. }


  11. //defaultProject

  12. dependencies {//DependencyHandler

  13.    compile gradleApi()//DependencyHandler有个gradleApi函数,还有几个其他的

  14. }

configuration代表的是一组文件的集合以及它的依赖, ConfigurationContainerNamedDomainObjectContainer子类,主要用于创建和管理configuration。configurations{闭包}处理流程和 DefaultSourceContainerDefaultTaskContainer容器都差不多,只是具体细节和目的不同。在处理myConfiguration会创造一个不做任何事情的configuration, compile.transitive = true则会找到名字为compile的configuration进行处理。其余的都可以按照以前的思路进行处理了。

总结处理流程

其实,整个gradle评估的处理流程非常的类似,就是一个巨大的容器,属性的查找和方法调用都是在遍历容器,同时,子容器同样支持遍历的操作。


以上是关于gradle实践-build.gradle解析原理的主要内容,如果未能解决你的问题,请参考以下文章

Android Studio 中build.gradle文件的详细解析

即使在 build.gradle 中指定,也无法解析符号“EnableJpaRespositories”

同步 gradle 的问题:无法解析外部依赖项 com.android.tools.build:gradle,因为没有定义存储库

无法在 app/build.gradle 中在线导入 com.android.build.OutputFile 解析符号“build”

gradle 不会从我在 build.gradle 中指定的库中的依赖版本中的父属性中解析占位符

> 无法解析 com.android.tools.build:gradle:3.5.0