Gradle 创建扩展属性详解

3,184 阅读13分钟

在自定义Gradle 插件的时候,总是期望外面能够传递一些指定的参数给插件,这样就可以按照需要进行动态配置了。这样插件也就更加通用,灵活并且适用性更广,所以了解扩展属性的创建也是非常有必要的。

而要实现参数的动态配置,就需要插件对外提供 扩展属性,即为 Project 创建 Extension。而 Extension (扩展属性)的创建,都是通过 ExtensionContainer 类的相关 Api 来创建的。下面就来详细的了解一下,扩展属性的创建。

一、创建扩展属性的常用方式


创建一个扩展属性的步骤如下:

  1. 创建一个扩展类:用于定义配置项,即指明用户可以配置的字段和调用的方法
  2. 通过 ExtensionContainer 的 Api 为目标对象(即Project对象)创建扩展:这样在使用插件时,就可以在build.gradle文件中配置扩展属性了。

通过上面的步骤,就可以创建出属于自己插件的扩展属性。

下面就通过一个简单的案例来说明一下:

1、简单的扩展属性

  • (1)创建一个扩展类:

    open class BigImageConfig {
    
        /**
         * 是否允许检测
         */
        var checkEnabled: Boolean = true
    
        /**
         * 大图大小
         */
        var maxSize: Long = 1024 * 1024
    }
    

    这里需要注意的一点是,扩展类不能是 final 类型的,即必须可以被继承,否则无法创建扩展属性。

  • (2)通过 ExtensionContainer 创建扩展

    class CheckBigImagePlugin : Plugin<Project> {
    
        override fun apply(project: Project) {
            // 创建配置项
            project.extensions.create("bigImageConfig", // 参数1
                                      BigImageConfig::class.java)// 参数2
        }
    }
    

    通过 ExtensionContainer 的 create 方法,创建扩展属性,参数说明:

    • 参数1: 扩展属性名,可以随意指定,建议根 扩展类保持一致,识别方便
    • 参数2: 扩展类的类类型

    如果 扩展类 BigImageConfig 的默认构造函数是有参数的,这就需要给构造函数传递所需要的参数,可以使用 ExtensionContainer 的create 重载方法,传递参数,如:

    open class BigImageConfig/**
         * 是否允许检测
         */
        var checkEnabled: Boolean = true
    ) {
        /**
         * 大图大小
         */
        var maxSize: Long = 1024 * 1024
    }
    // 插件
    class CheckBigImagePlugin : Plugin<Project> {
    
        override fun apply(project: Project) {
            // 创建配置项
            project.extensions.create("bigImageConfig", // 参数1
                                      BigImageConfig::class.java,// 参数2
                                     true)// 传递给 BigImageConfig 构造函数的参数
        }
    }
    

    这样就可以传递参数给扩展类了。

  • (3)扩展属性的使用

    通过上面的步骤,就可以创建一个名 bi gImageConfig 的扩展,在使用这个插件的时候,就可以在 build.gradle 文件中增加这个配置,如:

    bigImageConfig {
        checkEnabled = true
        maxSize = 1024
    }
    

    上面能配置的信息,都是我们在 扩展类 中定义的字段,所以要想增加别的配置信息,就在 扩展类 中继续增加字段就可以了。

    除了通过 等号赋值外,还可以直接用键值对的方式,即去掉等号,如:

    bigImageConfig {
        checkEnabled true
        maxSize 1024
    }
    

    不管是使用 键值对的方式 还是用 等号的方式 赋值,最终调用的都是属性的 setter 方法。

  • (4)扩展属性的获取

    经过第三步,我们配置了自己的扩展属性,那么在插件中,如何拿到这些扩展属性呢?这里可以通过 Project 的 property() 方法获取到,如:

    class CheckBigImagePlugin : Plugin<Project> {
        // 配置信息
        private lateinit var imageConfig: BigImageConfig
      
        override fun apply(project: Project) {
            // 创建配置项
            project.extensions.create("bigImageConfig", BigImageConfig::class.java)
            // 获取配置信息
            imageConfig = project.property("bigImageConfig") as BigImageConfig
        }
    }
    

    正如上面的代码所示,通过 Project 的 property() 方法,传入 扩展属性名(即在创建扩展属性的时候,传入的名称),就可以拿到指定的扩展属性。

    当然,如果插件用的是 Groovy 语言开发,还可以直接通过 project.扩展属性名 的方法,获取到扩展属性,如:

    // 获取配置信息
    def imageConfig = project.bigImageConfig
    

经过上面的步骤,我们就知道了如下三件事:

  • 创建扩展属性
  • 使用扩展属性
  • 获取扩展属性

需要注意的点:

  • 扩展类 必须是可以被继承的,不能是final类型

2、创建扩展属性的其它方式

上面通过案例,我们知道,通过 ExtensionContainer 的 create 方法,可以为插件创建 扩展属性,那么 ExtensionContainer 中,还有没有其它的方式可以用于创建 扩展属性呢? 当然有,我们可以看看 ExtensionContainer 的 主要Api:

public interface ExtensionContainer {

    /**
     * 添加一个扩展属性
     * @param name 扩展属性名
     * @param extension 扩展类 对象
     * @throws IllegalArgumentException When an extension with the given name already exists
     */
    void add(String name, Object extension);

    /**
     * 创建一个扩展属性
     * @param name 扩展属性名
     * @param type 扩展类类型
     * @param constructionArguments 扩展类的构造函数所需要的参数
     * @return 返回创建的扩展对象
     */
    <T> T create(String name, Class<T> type, Object... constructionArguments);

    /**
     * 通过名称,查找指定的扩展属性,如果没有找到,抛异常
     */
    Object getByName(String name) throws UnknownDomainObjectException;

    /**
     * 通过名称,查找指定的扩展属性,如果没有找到,返回null,不会抛异常
     */
    @Nullable
    Object findByName(String name);
}

上面截取了 ExtensionContainer 类的一部分Api,从上面的类声明中也可以看出,除了通过 create 方法创建扩展之外,还可以通过 add 方法创建扩展属性。

如:

class CheckBigImagePlugin : Plugin<Project> {
  
    override fun apply(project: Project) {
        // 创建配置项
        project.extensions.add("bigImageConfig", BigImageConfig::class.java)
    }
}

ExtensionContainer 的实现类 DefaultConvention 中,可以看到 add 方法的具体实现:

@Override
public void add(String name, Object extension) {
    if (extension instanceof Class) {
        create(name, (Class<?>) extension);
    } else {
        addWithDefaultPublicType(name, extension);
    }
}

可以看到,如果传递的是一个 扩展对象,那么直接添加,如果传递的是一个 Class 类类型,则通过 create 方法进行创建

3、总结

扩展属性创建方式总结:

  1. 通过 ExtensionContainer 的 create 方法进行创建
  2. 通过 ExtensionContainer 的 add 方法进行创建

两种方式都可以,区别在于:

  • 相同点:

    • 都可以通过键值对的方式进行配置,也可以使用等号的方式进行配置,最终调用的都是 属性的 setter 方法。

    • 都会抛异常:当需要创建的扩展属性已经存在的时候,即扩展属性重复,则都会抛出异常。

  • 不同点:

    • create 方法会返回创建 扩展对象,而 add 方法不会;

二、扩展属性嵌套


扩展属性嵌套,就是在扩展属性里面,还有扩展属性,就相当于实体类内部,引用了其它的实体类一样,如:

android {
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    viewBinding {
        enabled = true
    }
    dataBinding {
        enabled = true
    }
}

上面是Android 工程的 build.gradle 文件,通常都能见到的配置,其中 android 就是最外层的配置,defaultConfig ,buildTypes 和 viewBinding 等,就是 嵌套 在 android 下的扩展属性,这还是一层嵌套,还有多层嵌套,如 release 就是二层嵌套了。

像上面那样的嵌套扩展属性,也有多种方式可以实现,总结来说有如下方式:

  1. 在外层对象的构造函数中,通过 ExtensionContainer 的 api 创建扩展属性
  2. 在外层类中,定义一个方法,名字为配置项的名称,参数为 Closure ,泛型参数为嵌套的 扩展类
  3. 在外层类中,定义一个方法,名字为配置项的名称,参数为 Action, 泛型参数为嵌套的 扩展类

详细使用如下

1、通过 ExtensionContainer 的 api 创建扩展属性

在创建外层扩展属性的时候,通过 ExtensionContainer 的 api 同步创建 嵌套扩展属性,即在外层扩展类的构造函数中,创建扩展属性,如:

扩展类:

// 外层扩展类
class User {

    String name = ""

    int age = 10

    User() {
      // 在 创建 User 对象的时候,就创建 嵌套扩展 属性
        extensions.create("friendConfig", // 嵌套扩展属性的名称
                          Friend.class, // 嵌套扩展属性的类类型
                          "朋友是张三")// 嵌套扩展类的构造参数
    }
}

// 嵌套的扩展类
class Friend {

    String friendName = "ss"

    Friend(String friendName) {
        this.friendName = friendName
    }
}

在插件中,创建 扩展属性:

project.extensions.create("userConfig", User.class)

配置代码:

userConfig {
    name  "张三"
    age 20
    friendConfig {
        friendName = "Tom"
    }
}

测试代码:

task test2 {
    doLast {
        println "${project.userConfig.name}" // 可以正常拿到配置的用户名
        println "${project.userConfig.friendConfig.friendName}" // 也可以拿到朋友的名称
    }
}

打印的信息:

张三 Tom

注意:通过 ExtensionContainer 的 api 创建的扩展属性,可以省略 属性的等号,如:

userConfig {
    ...
    friendConfig {
        friendName  "Tom" // 省略等号,也是可以的,这就更像配置了,虽然省略了等号,但调用的还是 属性的 setter 方法
    }
}

2、在外层类中,定义一个方法,名字为配置项的名称,参数为 Closure ,泛型参数为嵌套的 扩展类

这种方式创建扩展属性的步骤:

  1. 创建一个嵌套扩展类的对象
  2. 定义一个配置方法,方法名为:配置项的名称,可以随意指定,参数为闭包,泛型参数为嵌套扩展类

2-1、创建对象

  • 直接在定义属性的时候,new 出对象

    class User {
    
        String name = ""
    
        int age = 10
      
      // 创建一个嵌套的扩展类对象
        Friend friend = new Friend()
    }
    
  • 通过Gradle 的 ObjectFactory 类,创建扩展类对象,如:

    class User {
    
        String name = "jj"
    
        int age = 10
      
        Friend friend
    
        User(Project project) {
          // 通过 ObjectFactory 创建 扩展类对象
            friend = project.objects.newInstance(Friend)
        }
    }
    
    class Friend {
    
        String friendName = "ss"
    
      // 构造函数
        @Inject Friend() {
        }
    }
    

    值得注意的是,通过 ObjectFactory 创建 扩展类对象的时候,需要给扩展类的构造函数,增加 @Inject 注解,否则会报异常。

2-1、创建一个配置方法

创建一个配置方法,方法名就是在配置文件中配置的扩展属性名,可以根据自己的喜好定义,参数为 Closure ,闭包的泛型参数为 扩展类,如:

class User {

    String name = ""

    int age = 10
  
  // 创建一个嵌套的扩展类对象
    Friend friend = new Friend()

  // 2. 定义一个配置方法,方法名按照自己喜好定义,参数是一个闭包对象,泛型类型是扩展类类型
    void friendConfig(Closure<Friend> closure) {
      // 通过 ConfigureUtil 类,为对象 创建 配置
        org.gradle.util.ConfigureUtil.configure(closure, 
                                                friend)// 扩展对象
    }
}

方法名:friendConfig,在配置的时候,使用的也是这个名称,

参数:Closure closure

实现:直接调用 gradle 的 ConfigureUtil 的 configure 方法执行 闭包参数,并传递创建的扩展对象。

配置代码如下:

userConfig {
    name  "张三"
    friendConfig {
        friendName = "Tom"
    }
}

注意

  • 为了使用的方便,最好是让创建的扩展对象的名称与配置方法的名称保持一致,这样配置和获取都能保持一致,如:

    class User {
      // 扩展对象
        Friend friendConfig = new Friend()
    
      // 配置方法
        void friendConfig(Closure<Friend> closure) {
            org.gradle.util.ConfigureUtil.configure(closure, friendConfig)
        }
    }
    
  • 这种方式创建的扩展属性,配置的时候,等号不能省略,即:

    userConfig {
        friendConfig {
            friendName "Tom" // 这种方式不能使用,会报错
        }
    }
    

3、在外层类中,定义一个方法,名字为配置项的名称,参数为 Action, 泛型参数为嵌套的 扩展类

这种方式跟第二种方式类似,配置方法的实现上有一些差异,具体如下:

class User {

    String name = "jj"
    int age = 10
    User(Project project) {
      // 创建扩展对象
        friend = project.objects.newInstance(Friend)
    }
  
    Friend friend 
  
  // 配置方法
    void friendConfig(Action<Friend> action) {
        action.execute(friend) // 直接执行 action 参数的 execute 方法,并传入扩展对象
    }
}

配置代码:

userConfig {
    name  "张三"
    friendConfig {
        friendName = "Tom"
    }
}

通过把配置方法的参数替换为 Action 对象,并执行action的exectute方法,其它不变。配置的时候,等号也是不能省略

4、总结

  1. 通过 ExtensionContainer 的 api 的方式,创建的嵌套扩展属性,在配置的时候,可以省略等号,而通过定义配置方法的方式创建的扩展属性,等号不能省略
  2. 通过 ObjectFactory 方法创建扩展对象的时候,扩展类的构造函数必须使用 @Inject 注解标注
  3. 扩展属性名和配置方法名,最好保持一致,方便使用

三、容器类型的扩展属性


在进行 Android 配置时,我们一定用过 buildTypes 的配置,类似:

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    debug {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

这种类型可以用于在代码块中创建新的指定类型的对象。

先来看一下源码:

public void buildTypes(Action<? super NamedDomainObjectContainer<BuildType>> action) {
    this.checkWritability();
    action.execute(this.buildTypes);
}

它传入的是一个 BuildType 类型列表的闭包代码。

NamedDomainObjectContainer 是一个容器,追根溯源它是继承自 Collection<T>。我们这里叫它命名对象容器,可以用于buildScript 中创建对象,且创建的对象必须要有 name 属性作为容器内元素的标识。

怎么来得到这样的容器对象呢?我们来看一下 Projectcontainer 方法:

<T> NamedDomainObjectContainer<T> container(Class<T> var1);
<T> NamedDomainObjectContainer<T> container(Class<T> var1, NamedDomainObjectFactory<T> var2);
<T> NamedDomainObjectContainer<T> container(Class<T> var1, Closure var2);

下面通过实例来介绍一下。

1、实例介绍

其实,创建一个容器类型的扩展属性,跟创建普通的扩展属性类似,也是分为两步走:

  1. 定义容器扩展类
  2. 创建扩展对象
  3. 添加扩展对象到 extension 中

具体如下所示:

1-1、定义 容器 扩展类

首先创建一个容器类型的扩展类 PhoneConfig ,上面说了,这个类必须有个 name 属性:

class PhoneConfig {

    String name

    String color

    float price

    PhoneConfig(String name) {
        this.name = name
    }
}

注意,name 字段必须要有。

1-2、创建容器扩展对象

通过 Project 的 container 方法,创建容器扩展对象,如:

// 通过 project 的 container 方法,创建容器对象
NamedDomainObjectContainer<PhoneConfig> phoneConfig = project.container(PhoneConfig.class)

1-3、创建容器扩展属性

即把创建的容器扩展对象,添加到extension 中,如:

project.extensions.add("phoneConfig", phoneConfig)

1-4、完整代码

完整的代码如下:

class User {

    String name = ""
    int age = 10
    
    User(Project project) {
        friend = project.objects.newInstance(Friend)
      // 创建容器对象
        NamedDomainObjectContainer<PhoneConfig> phoneConfig = project.container(PhoneConfig.class)
      // 把容器对象添加到 extensions中
        project.extensions.add("phoneConfig", phoneConfig)
    }

    Friend friend

    void friendConfig(Action<Friend> action) {
        action.execute(friend)
    }
}

配置代码:

userConfig {
    name "张三"
    
    phoneConfig {
        iphone {// 名字
            color = "红色" // 等号不能省略
            price = 10000
        }
        xiaomi {
            color = "黄色"
            price = 5000
        }
    }
}

获取配置信息:

task test2 {
    doLast {
        println "${project.userConfig.name}"
      // 直接通过 project 对象去拿 phoneConfig
        project.phoneConfig.forEach { phone ->
            println "name=${phone.name},color=${phone.color},price=${phone.price}"
        }
    }
}

打印信息如下:

张三 name=iphone,color=红色,price=10000.0 name=xiaomi,color=黄色,price=5000.0

我们注意到,上面的容器配置,都要写上等号,否则就会报错,那有没有办法,不写等号也可以呢?当然有,给 扩展类 定义一个跟属性同名的 赋值方法就可以了,如:

class PhoneConfig {

    String name

    String color

    float price

    PhoneConfig(String name) {
        this.name = name
    }

  // 定义一个跟 属性同名的配置方法
    def color(String color) {
        this.color = color
    }
}

配置代码:

userConfig {
    name "张三"
    phoneConfig {
        iphone {// 名字,这个就是容器里面的item的名称,
            color  "红色"
            price = 10000
        }
        xiaomi {
            color  "黄色"
            price = 5000
        }
    }
}

定义了跟属性同名的赋值方法之后,就可以把等号省略了,如 color 属性,而price 属性没有定义同名的 赋值方法,则等号不能省略。

这里创建的容器属性,没有保存在外层扩展类中,如果想在外层类中也能访问容器类,则需要在外层类中保存容器类对象,如:

class User {
		NamedDomainObjectContainer<PhoneConfig> phoneConfig 
  
    User(Project project) {
      // 创建容器对象
        phoneConfig = project.container(PhoneConfig.class)
      // 把容器对象添加到 extensions中
        project.extensions.add("phoneConfig", phoneConfig)
    }
}

访问:

task test2 {
    doLast {
        println "${project.userConfig.name}"
      // 通过userConfig拿到他的属性 phoneConfig对象
        project.userConfig.phoneConfig.forEach { phone ->
            println "name=${phone.name},color=${phone.color},price=${phone.price}"
        }
    }
}

到此,扩展属性相关的东西就介绍完了。

四、总结:

1、 扩展属性的创建方式

  • 通过 ExtensionContainer 的 create 或者 add 方法 创建扩展属性

2、扩展属性嵌套的创建方式

  • 通过 ExtensionContainer 创建
  • 通过定义配置方法,创建嵌套属性

3、容器扩展的创建方式

  • 通过 Project 的 container 方法,创建 容器对象,并把创建的扩展对象添加到 ExtensionContainer 容器中。

4、Android Gradle Plugin 官方文档