gradle实践-build.gradle解析原理

/ android / 没有评论 / 25浏览

前言

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

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

groovy的MOP和闭包

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

GroovyObject

GroovyObject是所有grovvy对象的基类

public interface GroovyObject {
    Object invokeMethod(String name, Object args);
    Object getProperty(String propertyName);
    void setProperty(String propertyName, Object newValue);
    MetaClass getMetaClass();
    void setMetaClass(MetaClass metaClass);
}

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

class Person implements GroovyInterceptable {
    //GroovyInterceptable接口仅仅是一个标示,代表无论此对象是否有定义好的方法,
    //都会调用invokeMethod方法,优先拦截作用
    def name
    def mapForStoreUnKnowProperty = new HashMap()//存储动态属性
    Person(name) {
        this.name = name
    }

    @Override
    Object getProperty(String s) {
        System.out.println "getProperty $s"
        if (s == "name") {
            return this.name
        }
        //如果动态属性也没有,那么就返回unknow
        if (mapForStoreUnKnowProperty.containsKey(s)) {
            mapForStoreUnKnowProperty.get(s)
        } else {
            "unknow property $s"
        }

    }
    //如果没有此属性,那么使用动态属性容器进行存储。
    @Override
    void setProperty(String s, Object o) {
        System.out.println "setProperty $s"
        if (s == "name") {
            this.name = o
        } else {
            mapForStoreUnKnowProperty.put(s, o)
        }
    }

    //拦截方法
    @Override
    Object invokeMethod(String s, Object o) {
        System.out.println "invokeMethod $s $o"
        if (s == "sayHelloWord") {
            System.out.println "hello word"
        } else {
            System.out.println "no method"
        }
    }

    static void main(String[] args) {
        def person = new Person("god")
        System.out.println "person ${person.name} ${person.noProperty}"
        person.noProperty = "动态属性value"
        System.out.println "person  ${person.noProperty}"
        person.sayHelloWord()
    }
}

输出内容:
getProperty name
getProperty noProperty
person god unknow property noProperty
setProperty noProperty
getProperty noProperty
person  动态属性value
invokeMethod sayHelloWord []
hello word

MetaClass

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

public interface MetaClass extends MetaObjectProtocol {
    //拦截方法
    Object invokeMethod(Class var1, Object var2, String var3, Object[] var4, boolean var5, boolean var6);
    //拦截属性
    Object getProperty(Class var1, Object var2, String var3, boolean var4, boolean var5);
    void setProperty(Class var1, Object var2, String var3, Object var4, boolean var5, boolean var6);
    //如果没有重写invokeMethod同时又没有定义此方法,会回调到此处
    Object invokeMissingMethod(Object var1, String var2, Object[] var3);
    ....//省略一些~~
}

所有的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类增加__meta_class__,__dyn_obj__等属性,并提供get方法。

build.gradle解析入口

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

屏幕快照 2018-11-25 下午11.19.13.png

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

DynamicObject 动态对象

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

屏幕快照 2018-11-26 下午3.25.18.png

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

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中有个很重要的容器object DynamicObject[],它一般含有四个对象

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

  1. BeanDynamicObject此对象方法和属性的查找委托给了自身代理的Bean也就是project对象,bean通过MetaClass进行查找。
  2. ExtraPropertieDynamicObjectAdapter只能读取/修改属性,不能代理方法,此处代理的对象其实是DefaultConvention中的extensionsStorage里的ext,其实应该是为了加快属性读取和修改效率的。因为这两个对象是同一个。
  3. DefaultConvention.ExtensionsDynamicObjectDefaultConvention负责管理插件提供的约束(DSL配置),同时还负责extensionsStorage中扩展配置,例如:ext的扩展属性,reporting的报告配置等。
  4. DefaultTaskContainer(DefaultNamedDomainObjectCollection).ContainerElementsDynamicObject 存储所有task信息,支持task的属性访问和方法调用

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

第一部分 project属性配置

plugins {
    id 'java'  //java插件
    id 'com.demon.yu'  //自定义的插件
}//不做分析
/**第一部分*/
//defaultProject中的方法
group 'com.demon.yu.gradle'
project.version '1.0-SNAPSHOT'
//defaultProject属性
rootProject.version = "1.0-SNAPSHOT"

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

    @Override
    public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {
        for (DynamicObject object : objects) {
            DynamicInvokeResult result = object.tryInvokeMethod(name, arguments);
            if (result.isFound()) {
                return result;
            }
        }
        return DynamicInvokeResult.notFound();
    }

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

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

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

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

第二部分 ext属性配置

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

//ext是DefaultExtraPropertiesExtension
ext.vertionCode = 1
ext {//闭包环境为DefaultExtraPropertiesExtension
    targetSDK = 28
}

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

@Override
public DynamicInvokeResult tryGetProperty(String name) {
    Object extension = extensionsStorage.findByName(name);
    if (extension != null) {
        return DynamicInvokeResult.found(extension);
    }
    ...//省略其他的
    return DynamicInvokeResult.notFound();
}

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会存储相应键值对数据了。 对于代码段

ext {
    targetSDK = 28
}

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

@Override
public DynamicInvokeResult tryInvokeMethod(String name, Object... args) {
    if (isConfigureExtensionMethod(name, args)) {   //判断包含ext同时第1个参数是闭包,那么就会执行configureExtension(name, args)
        return DynamicInvokeResult.found(configureExtension(name, args));
    }
    ....
    return DynamicInvokeResult.notFound();
}
private boolean isConfigureExtensionMethod(String name, Object[] args) {
    return args.length == 1 && args[0] instanceof Closure && extensionsStorage.hasExtension(name);
}

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

public class ExtraPropertiesDynamicObjectAdapter extends AbstractDynamicObject {
    private final ExtraPropertiesExtension extension;//和defaultConvention中extensionsStorage存储的ext对应对应的DefaultExtraPropertiesExtension是同一个对象。
    public ExtraPropertiesDynamicObjectAdapter(Class<?> delegateType, ExtraPropertiesExtension extension) {
        this.delegateType = delegateType;
        this.extension = extension;
    }
    ...//省略其他的
    @Override
    public DynamicInvokeResult tryGetProperty(String name) {
        if (extension.has(name)) {
            return DynamicInvokeResult.found(extension.get(name));
        }
        return DynamicInvokeResult.notFound();
    }
    @Override
    public DynamicInvokeResult trySetProperty(String name, Object value) {
        if (extension.has(name)) {
            extension.set(name, value);
            return DynamicInvokeResult.found();
        }
        return DynamicInvokeResult.notFound();
    }
    ...//省略其他
}

第三部分 DefaultConcention属性配置

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

/**第三部分*/
//javaPluginConvention
sourceCompatibility = 1.8
//javaPluginConvention //执行的context
sourceSets {
    //sourceSetContainer
    main {
        //sourceSet
        java {
            //sourceDirectorySet
            srcDirs = ["src/java"]
            exclude 'some/unwanted/package/**'
        }
    }
    namain{java{}}//可以随便写的
}

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

 @Override
public DynamicInvokeResult trySetProperty(String name, Object value) {
    checkExtensionIsNotReassigned(name);
    if (plugins == null) {
        return DynamicInvokeResult.notFound();
    }
    for (Object object : plugins.values()) {
        BeanDynamicObject dynamicObject = asDynamicObject(object).withNotImplementsMissing();//以Convention对象封装一个BeanDynamicObject实例,然后执行属性设值。
        DynamicInvokeResult result = dynamicObject.trySetProperty(name, value);
        if (result.isFound()) {
            return result;
        }
    }
    return DynamicInvokeResult.notFound();
}

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

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

屏幕快照 2018-11-29 下午6.04.58.png

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

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

第四部分 task定义配置相关

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

//直接反射到defaultProject的task方法
task helloWord(dependsOn: 'jar') {
    //DefaultTask
    println("配置task helloWord时执行")
    group="groupName"
    description = "任务描述"
    doLast {
        println("执行helloWord doLast")
    }
}
println helloWord.description

//很多种定义task的写法,具体可以看下DefaultProject的实现
def map = ["dependsOn": 'helloword']
task map, "helloNext", {
}
task noThing

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

第五部分 其他

第五部分简单说下gradle的configuration

/**第五部分*/
//configurationContainer是NamedDomainObjectContainer。sourceSetContainer类似的东西
configurations {
    myConfiguration
    compile.transitive = true
}

//defaultProject
artifacts { //DefaultArtifactHandler
    myConfiguration jar   //jar是task名字,根据源码还可以支持很多种类型,
}

//defaultProject
dependencies {//DependencyHandler
    compile gradleApi()//DependencyHandler有个gradleApi函数,还有几个其他的
}

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

总结处理流程

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