1、前言

Gradle插件开发在Android进阶知识中是占有必定份额的,特别是在功用优化领域,根本都会涉及,并且跟咱们日常的编译打包也休戚相关,加上有不少招聘要求里也清晰要有Gradle插件开发经验,所以即便大部分人的日常开发中或许用不到插件开发,但也心向往之。本文就给咱们带来Gradle系列的第8篇——插件开发攻略,希望能给咱们带来收成。

【Gradle-8】Gradle插件开发指南

2、Gradle插件是什么

Gradle插件(Plugin)是一种用于扩展和定制 Gradle构建体系功用的机制。Gradle是一个强壮的构建主动化工具,用于构建和办理各种类型的项目,从简略的Java运用到杂乱的多模块企业级项目。插件为Gradle提供了灵活性,答应开发者依据特定需求增加自界说行为和功用。

Gradle插件能够履行各种任务,包含编译代码、履行测试、打包文件、生成文档等等。插件能够访问和操作 Gradle的构建模型,如项目、任务、依靠关系等,然后完结对构建进程的操控和定制。

Gradle提供了丰厚的插件生态体系,能够运用现有的官方插件或第三方插件来增强构建进程。许多盛行的结构和工具,如 Android、Spring Boot、Kotlin 等,都有相应的Gradle插件,使得与这些技能栈的集成变得愈加简略和高效。

比如咱们了解的Android插件com.android.application

plugins {
    id 'com.android.application'
}

经过编写自己的Gradle插件,你能够定制和扩展 Gradle 构建体系,以习惯特定项目的需求。你能够在插件中界说自界说任务、装备扩展、操作项目特点、运用其他插件等。插件使得构建进程变得可控和可定制,然后进步开发功率。

3、为什么要写插件

写插件的含义:

  1. 封装,把具体的逻辑抽出去,项目只需运转插件就行了,不必放在某一个build.gradle文件中,而下降build.gradle的可读性;
  2. 复用,把通用的逻辑抽出去,用的时分只需apply运用插件即可,不必一遍一遍的复制,也能够提供给其他项目运用;
  3. 定制:假如需求在编译期做一些插桩、Hook之类的自界说操作,也需求用到编译插件;

4、插件写在哪

上文咱们介绍了Gradle Task,其中有说到Task写在哪,那Plugin又写在哪呢?

插件Plugin能够写在3个地方:

  1. 跟Task相同,写在build.gradle文件中,作用域当时Project;
  2. 写在buildSrc里,作用域当时项目一切Project;
  3. 写在独自项目里,发布后可提供给一切项目一切Project;

依据自己需求,结合插件作用域,写在不同的位置即可。

5、自界说插件

编写一个插件Plugin其实挺简略的,只需求完结Plugin接口,并完结唯一apply办法即可。

咱们就直接写在build.gradle文件中:

class YechaoaPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
    }
}
apply plugin: YechaoaPlugin
//apply(plugin: YechaoaPlugin)

这实践上是一个内联类。

写完别忘了apply依靠上。第9行的apply办法是调用的PluginAware接口的apply()办法,参数是一个map,用来映射Plugin Id。

sync输出:

> Configure project :app
这是插件:YechaoaPlugin
...

在上一文Task详解中说到,Task是Project中的一个办法,所以咱们需求经过Project去创立一个Task。示例中YechaoaPlugin类完结了Plugin接口并完结唯一apply办法,而apply办法中提供了Project目标,那咱们就也能够在Plugin中去创立一个Task。

class YechaoaPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件:${this.class.name},它创立了一个Task:${task.name}")
            }
        }
    }
}

如上,咱们在Plugin里创立了一个Task,这时分sync是不会履行Task里边的打印的,得独自去履行这个Task。

履行:

./gradlew YechaoaPluginTask

输出:

> Task :app:YechaoaPluginTask
这是插件:YechaoaPlugin,它创立了一个Task:YechaoaPluginTask

ok,最根本的Plugin编写便是这么简略。

结合以上两次输出,不管是单纯的在Plugin里打印也好,仍是在Plugin里创立Task,当咱们依靠YechaoaPlugin插件的时分,即apply plugin: YechaoaPlugin,这个apply会把这个插件放进PluginContainer里,类似TaskContainer,一起这个apply也是在编译阶段履行Plugin接口的apply()办法,所以sync履行构建后会有输出,履行的Task也是在有向无环图里。

6、自界说插件扩展

在Gradle系列的第二章里,经过源码分析了android{ }闭包是怎样来的,android{ }闭包是咱们非常了解的装备,经过DSL的办法,咱们常常会在里边装备compileSdk、buildTypes等。

而在自界说插件的时分常常也会有这种自界说装备的需求,经过这些自界说的装备能够让咱们的插件提供更丰厚的才干。这些装备便是经过扩展插件来的。

6.1、界说扩展目标

interface YechaoaPluginExtension{
    Property<String> getTitle()
}

能够是一个接口,也能够是一个类。

6.2、把扩展增加给Plugin并运用

class YechaoaPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创立了一个Task:${task.name}")
                println(extension.title.get())
            }
        }
    }
}

project.extensions.create()办法接收两个参数:

  1. 第一个是姓名,比如yechaoa、android;
  2. 第二个便是扩展目标,然后回来这个扩展目标,经过这个扩展目标的办法就能够获取自界说的装备参数;

6.3、装备参数

yechaoa.massage = "【Gradle-8】Gradle插件开发攻略"

一个装备能够直接省掉写,也能够这么写

yechaoa {
  	massage = "【Gradle-8】Gradle插件开发攻略"
}

假如没有设置装备参数的话,Gradle也提供了默许值的设置:

extension.title.convention("默许装备title")

假如是类目标,就界说一下setter/getter。

假如有多个装备怎样写?扩展多个装备特点就好了。

6.4、嵌套扩展

如下,android { }里边还有defaultConfig { }

android {
    namespace 'com.yechaoa.gradlex'
    compileSdk 32
    defaultConfig {
        applicationId "com.yechaoa.gradlex"
        ...
    }
}

嵌套扩展其实也很简略,便是套娃。

上面咱们用接口界说了扩展特点,下面换一种写法,用class目标来界说。

6.4.1、界说扩展

class YechaoaPluginExtension {
    String title
    int chapter
    SubExtension subExtension
    YechaoaPluginExtension(Project project) {
        subExtension = project.extensions.create('sub', SubExtension.class)
    }
}
class SubExtension {
    String author
}

多界说了一个SubExtension类,然后在YechaoaPluginExtension实例化的时分加到ExtensionContainer中。

假如要类嵌套的话也行,得是内联类,不然编译识别不了。

6.4.2、获取扩展特点

class YechaoaPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        // 设置默许值 能够界说set()办法 然后在这里set
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创立了一个Task:${task.name}")
                println("title = ${extension.title}")
                println("chapter = ${extension.chapter}")
                println("author = ${extension.subExtension.author}")
            }
        }
    }
}

比较于上面接口界说的示例,少了Property目标的.get(),也去掉了设置的默许值的环节,假如想要的话,在类目标里界说setter/getter办法就行,其它逻辑不变。

6.4.3、运用

yechaoa {
    title = "【Gradle-8】Gradle插件开发攻略"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

闭包装备中,多了一个sub{ }闭包,便是咱们YechaoaPluginExtension类中界说的。

6.4.4、履行

./gradlew YechaoaPluginTask

6.4.5、输出

> Task :app:YechaoaPluginTask
title = 【Gradle-8】Gradle插件开发攻略
chapter = 8
author = yechaoa

6.4.6、完整代码

class YechaoaPluginExtension {
    String title
    int chapter
    SubExtension subExtension
    YechaoaPluginExtension(Project project) {
        subExtension = project.extensions.create('sub', SubExtension.class)
    }
}
class SubExtension {
    String author
}
class YechaoaPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        // 设置默许值 能够界说set()办法 然后在这里set
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创立了一个Task:${task.name}")
                println("title = ${extension.title}")
                println("chapter = ${extension.chapter}")
                println("author = ${extension.subExtension.author}")
            }
        }
    }
}
apply plugin: YechaoaPlugin
yechaoa {
    title = "【Gradle-8】Gradle插件开发攻略"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

现在yechaoa{ }这个装备是不是很了解了:

yechaoa {
    title = "【Gradle-8】Gradle插件开发攻略"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

是不是跟android{ }一毛相同:

android {
    namespace 'com.yechaoa.gradlex'
    compileSdk 32
    defaultConfig {
        applicationId "com.yechaoa.gradlex"
        ...
    }
}

7、编写在独自项目中

上面咱们的Plugin是写在build.gradle文件中的,而一般在实践项目中,为了更好的复用,一般都是写在buildSrc或许独自的项目中。

而写在build.gradle文件中和写在buildSrc或许独自的项目中仍是有一些区其他,下面带咱们一起看下在独自项目中是假如来写的(等同于buildSrc)。

来个简略的,就写一个打印项目中一切依靠的Plugin吧。

7.1、新建Module

新建一个名称为plugin的Module,类型选择为Library或下面的Java or Kotlin Library

【Gradle-8】Gradle插件开发指南

新建Module之后,会有默许的文件目录,多余的文件都能够删掉的。

【Gradle-8】Gradle插件开发指南

咱们能够看到main文件夹下有java文件夹,Gradle Plugin能够用java写,也能够用kotlin、groovy来学,喜欢用什么就能够在main文件下新建对应言语的文件夹接口,比如kotlin文件夹。

7.2、新建文件增加依靠

7.2.1、新建类

新建一个DependenciesPlugin类:

【Gradle-8】Gradle插件开发指南

可是这时分仍是不能编写Plugin的,由于你这个module里边并没有依靠Gradle相关API。

7.2.2、增加依靠

Gradle 6.4及以后就不必再增加gradleApi()来装备Plugin的依靠啥的了,直接一个java-gradle-plugin插件搞定,它会主动把javagradleApi()依靠增加到项目中。

并且不需求像以前在src/main/resources/META-INF/gradle-plugins/xxx.properties中来装备你的implementation-class了,直接一个gradlePlugin{ }装备搞定,Gradle会主动生成META-INF描绘文件。

在plugin>build.gradle文件中依靠插件:

plugins {
    id 'java-gradle-plugin'
}

装备如下:

gradlePlugin{
    plugins{
        DependenciesPlugin{
            id = 'com.yechaoa.plugin.dependencies'
            implementationClass = 'com.yechaoa.plugin.DependenciesPlugin'
        }
    }
}
  • id:apply时引证的plugin id;
  • implementationClass:Plugin途径;

在Gradle 6.4以前:

【Gradle-8】Gradle插件开发指南

implementation-class=com.yechaoa.plugin.DependenciesPlugin

由于以前这些文件夹、装备全是手动的,很繁琐,比较之下,现在更爽多了。

7.3、编写Plugin

package com.yechaoa.plugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
/**
 * GitHub : https://github.com/yechaoa
 * CSDN : http://blog.csdn.net/yechaoa
 * <p>
 * Created by yechao on 2023/8/8.
 * Describe :
 */
class DependenciesPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println(">>>>>>>>  " + this.getClass().getName());
    }
}
  1. 新建一个类完结Plugin接口;
  2. 在apply办法中完结自己的逻辑,这里示例打印;

到此,Plugin的根本雏形就有了。

增加依靠运用:

apply plugin: 'com.yechaoa.plugin.dependencies'

可是现在外部项目还用不了,直接引证这个 会呈现找不到plugin的状况(not found)。

Plugin with id 'com.yechaoa.plugin.dependencies' not found.

由于这个Plugin是在独自项目中写的,精确的来说,跟其他项目其实是没有关系的,想要找到这个插件,就得发布这个插件才行。

7.4、本地发布

本地发布要比远端发布简略多了,尽管远端发布也不难,仅仅繁琐。

7.4.1、Maven插件

首要,比较常用的库房是maven,在plugin>build.gradle文件中先依靠一个maven发布的插件'maven-publish'

plugins {
    id 'maven-publish'
}
dependencies {
    implementation 'com.android.tools.build:gradle:7.3.0'
}

7.4.2、发布装备

增加发布装备

group 'com.yechaoa.plugin'
version '1.0.0'
publishing {
    // 装备Plugin GAV
    publications {
        maven(MavenPublication) {
            groupId = group
            artifactId = 'dependencies'
            version = version
            from components.java
        }
    }
    // 装备库房地址
    repositories {
        maven {
            url layout.buildDirectory.dir("maven-repo")
        }
    }
}

7.4.3、履行发布操作

./gradlew publish

或许在Android Studio右边Gradle可视化的面板点击运转publish

【Gradle-8】Gradle插件开发指南

7.4.4、生成产物

【Gradle-8】Gradle插件开发指南

ok,这时分build文件夹下已经有本地发布装备的maven-repo文件夹了。

能够再承认一下maven的元数据pom文件:

<metadata>
  <groupId>com.yechaoa.plugin</groupId>
  <artifactId>dependencies</artifactId>
  <versioning>
    <latest>1.0.0</latest>
    <release>1.0.0</release>
    <versions>
      <version>1.0.0</version>
    </versions>
    <lastUpdated>20230809154815</lastUpdated>
  </versioning>
</metadata>

7.5、运用

ok,本地发布完了,要想运用这个插件,跟咱们正常依靠插件是相同的流程。

三步走:

  1. 在settings.gradle文件中装备插件库房地址
pluginManagement {
    repositories {
        // ...
        maven {
            url './maven-repo'
        }
    }
}
  1. 在project>build.gradle文件中增加插件依靠
buildscript {
    dependencies {
        classpath('com.yechaoa.plugin:dependencies:1.0.0')
    }
}
  1. 在app:build.gradle文件中依靠咱们的plugin
plugins {
    id 'com.yechaoa.plugin.dependencies'
}

以上装备都是在app模块中增加的,即需求运用的模块。

编译看作用:

> Configure project :app
>>>>>>>>  com.yechaoa.plugin.DependenciesPlugin

ok,正确打印出来了,阐明咱们自界说的plugin已经能够对外提供运用了。

留意:本地依靠运用的时分,要先发布,再依靠插件,不然就会呈现cannot found找不到依靠的状况。

7.6、功用完结

上面的示例仅仅一个打印,持续完结咱们的功用,把一切的依靠项打印出来。

打印依靠项的办法有很多,比如gradle指令

./gradlew app:dependencies

那假如我要区分官方库和三方库怎样办呢,这时分就无法满足了。

下面改造一下上面的Plugin:

class DependenciesPlugin implements Plugin<Project> {
    private final String TAG = "DependenciesPlugin >>>>> ";
    @Override
    public void apply(Project project) {
        System.out.println(TAG + this.getClass().getName());
        DependenciesPluginExtension extension = project.getExtensions().create("printDependencies", DependenciesPluginExtension.class);
        project.afterEvaluate(pro -> {
            /*
             * 扩展的装备要在 project.afterEvaluate 之后获取哦
             * 由于装备阶段完结,才干读取参数
             * 且装备完结,才干拿到一切的依靠
             */
            // 默许敞开打印
            extension.getEnable().convention(false);
            if (extension.getEnable().get()) {
                // debug/release也能够加装备
                System.out.println(TAG + "已敞开依靠打印");
                AppExtension androidExtension = project.getExtensions().getByType(AppExtension.class);
                androidExtension.getApplicationVariants().all(applicationVariant -> {
                    System.out.println(TAG + ">>>>>>>>  applicationVariant.getName() = " + applicationVariant.getName());
                    // 办法一:build.gradle 文件中增加的依靠
                    Configuration configuration = project.getConfigurations().getByName(applicationVariant.getName() + "CompileClasspath");
                    Set<Dependency> allDependencies = configuration.getAllDependencies();
//                for (Dependency dependency : allDependencies) {
//                    System.out.println(TAG + "dependency === " + dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion());
//                }
                    List<String> androidLibs = new ArrayList<>();
                    List<String> otherLibs = new ArrayList<>();
                    // 办法二:一切的依靠,包含依靠中的依靠
                    configuration.getResolvedConfiguration().getLenientConfiguration().getAllModuleDependencies().forEach(resolvedDependency -> {
                        ModuleVersionIdentifier identifier = resolvedDependency.getModule().getId();
                        //System.out.println(TAG + "identifier === " + identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        if (identifier.getGroup().contains("androidx") || identifier.getGroup().contains("com.google") || identifier.getGroup().contains("org.jetbrains")) {
                            androidLibs.add(identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        } else {
                            otherLibs.add(identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        }
                    });
                    System.out.println("--------------官方库 start--------------");
                    androidLibs.forEach(System.out::println);
                    System.out.println("--------------官方库 end--------------");
                    System.out.println("--------------三方库 start--------------");
                    otherLibs.forEach(System.out::println);
                    System.out.println("--------------三方库 end--------------");
                });
            } else {
                System.out.println(TAG + "已封闭依靠打印");
            }
        });
    }
}

扩展:

interface DependenciesPluginExtension {
    Property<Boolean> getEnable();
}

运用:

printDependencies {
    enable = true
}

小结:

  1. 先是加了一个装备enable来判别是否需求打印依靠;
  2. 在项目评估完结之后(project.afterEvaluate),获取项目装备(Configuration);
  3. 经过Configuration获取一切的依靠(getAllModuleDependencies);
  4. 遍历获取GAV,并分类;
  5. 最终打印出来;

这里有一点需求留意,咱们需求在project.afterEvaluate办法中去获取扩展装备,由于apply plugin的履行时机早于扩展装备,不然获取不到扩展装备的值。

编译运转输出:

> Configure project :app
>>>>>>>>  com.yechaoa.plugin.DependenciesPlugin
>>>>>>>>  applicationVariant.getName() = debug
--------------官方库 start--------------
com.google.android.material:material:1.8.0
androidx.appcompat:appcompat:1.5.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10
...(省掉部分)
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1
androidx.arch.core:core-runtime:2.1.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1
--------------官方库 end--------------
--------------三方库 start--------------
com.squareup.okhttp3:okhttp:4.10.0
com.squareup.retrofit2:retrofit:2.9.0
com.squareup.okio:okio:3.0.0
com.squareup.okio:okio-jvm:3.0.0
--------------三方库 end--------------

ok,在独立项目中自界说插件,把一切的依靠区分并打印出来的作用就完结了。

8、总结

咱们先是介绍了Gradle插件,然后以最根本的写法上手,然后又介绍了Plugin扩展的完结和用法,最终以一个小例子介绍了Plugin在独立项目中的编写、发布和供外部运用的进程。

总体而言,难度一般,可是也有一些小细节需求留意,比如对Gradle生命周期的掌握、运用插件的流程等。

9、最终

有朋友反馈之前的文章尽管都写的很棒,可是篇幅有点长,不是很好消化。。

尽管可是,Gradle的东西确实很多,后面会酌情精简一些。

写作不易,感谢支撑~

10、GitHub

github.com/yechaoa/Gra…

11、相关文档

  • Developing Custom Gradle Plugins
  • Gradle Plugin Development Plugin
  • Using Gradle Plugins
  • 【Gradle-4】Gradle的生命周期
  • 【Gradle-7】Gradle构建中心之Task攻略