Theme Preview

Hue:

You are using an outdated browser that does not support OKLCH colors. The color setting will not take effect.

Gradle之旅(3):Gradle插件

4353 字

一、插件作用

Gradle插件可以做什么呢?主要有以下的几点

  • 为项目配置依赖。
  • 为项目配置约定,比如约定源代码的存放位置。
  • 为项目添加任务,完成测试、编译、打包等任务。
  • 为项目中的核心对象和其他插件的对象添加拓展类型。

使用Gradle插件主要有以下的好处:

  • 重用和减少维护在多个项目类似的逻辑的开销。
  • 更高程度的模块化。
  • 封装必要的逻辑,并允许构建脚本尽可能是声明性地。

二、插件分类

Gradle 中插件可以分为两类:脚本插件和二进制插件。要想应用插件,主要有两个步骤,其一:解析插件,其二:应用插件。下面就这两种类型的插件来做介绍。更多信息可参见官网

2.1 脚本插件

脚本插件 其实就是以 .gradle 为后缀的配置脚本,这里我们定义一个脚本插件:config.gradle:

ext{
myName = "myworld"
verson='1.0'
url='www.baidu.com'
}

这里只是定义了一个简单的脚本,事实上里面可以定义一些的任务去执行特定的操作。现在有了这样的一个脚本插件,该如何引用呢:

apply from: 'config.gradle'
task myTask {
doLast {
println "我的名字为:${myName}, 版本为:${verson}, 地址为:${url}"
}
}

可见对于脚本插件来说,是使用 apply from 的方式来引用,在根目录下执行 ./gradle myTask 即可打印出结果。这里来简单说一下 apply 方法,这个方法是定义在 PluginAware 接口中,原型如下:

public interface PluginAware {
PluginContainer getPlugins();

//闭包作为参数
void apply(Closure var1);
//配置一个ObjectConfigurationAction
void apply(Action<? super ObjectConfigurationAction> var1);
//Map作为参数
void apply(Map<String, ?> var1);

PluginManager getPluginManager();
}

这里 apply 可以接收三种不同的参数,那么对应的就应该有三种方式来引用插件:

// ① 闭包作为参数
apply {
from "config.gradle"
}

// ② 配置一个ObjectConfigurationAction
apply(new Action<ObjectConfigurationAction>() {
@Override
void execute(ObjectConfigurationAction objectConfigurationAction) {
objectConfigurationAction.from("config.gradle")
}
})

// ③ Map作为参数
apply from: "config.gradle"

2.2 对象插件

对象插件(或称二进制插件), 是继承 Plugin 接口的插件,对象插件可以分为内部插件和第三方插件。所谓内部插件是指 Gralde 内置的大量的常用插件,如官网核心插件所示,例如 java 插件和 C 的插件,我们可以直接引用:

apply plugin:'java'
apply plugin:'cpp

每一个插件在发布的时候都会指定唯一的一个插件ID,上面就是通过插件ID的方式来引用的,其实真正的 Java 插件全称是 JavaPlugin,应用方式如下:

apply plugin: JavaPlugin

第三方插件通常是以 Jar 包形式存在,要想让构建脚本知道第三方插件的存在,需要使用 buildscrip 来配置 classpath,如 Android Gradle 这个插件:

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
}
}
apply plugin: 'com.android.application'

在 buildscrip 中来定义插件所在的原始仓库和插件的依赖 ,再通过 apply 方法配置即可

三、自定义插件

脚本插件本质上就是一个 Gradle 构建脚本,这里不在赘述,本小节着重介绍如何实现一个对象插件。从官网Custom Plugins上可知,有如下三个地方可以用来存放插件的源码:

3.1 Build script

这种方式是直接在已有的 gradle 构建脚本中直接添加插件代码(以build.gradle为例):

class MyPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println("MyPlugin 执行了")

project.task("MyTask"){
doLast {
println("MyTask执行了")
}
}
}
}

apply plugin: MyPlugin

这个就是直接在build.gradle 中实现了一个插件,在插件中定义了一个MyTask任务,但是这个插件只能在本文件中使用,我们直接执行Mytask任务 ./gradlew MyTask ,得到如下结果:

> Configure project :app
MyPlugin 执行了

> Task :app:MyTask
MyTask执行了

这种方式并不常用,虽然这种形式的插件会自动编译并包含在构建脚本的类路径中,但这种方式局限性太大,只能本 Project 使用,其他的 Project 不能使用

3.2 buildSrc project

buildSrc 是默认的插件目录,Gradle 会自动编译和测试其中的代码并将其添加到构建脚本的 classpath 中,因此不需要单独 included build 。相较于脚本插件,此种类型更容易维护、重构和测试。

一个包含 buildSrc 的工程,其项目结构如下。buildSrc下 的代码使用跟应用代码类似的包。如果有额外的配置需要,buildSrc 目录可以放一个可选的构建脚本(比如,使用插件或声明依赖):

.
├── build.gradle
├── buildSrc
│   ├── build.gradle
│   └── src
│   └── main
│   └── java
│   └── com
│   └── zsk
│   └── plugin
│   └── MyPlugin.java
├── otherModule
├── gradlew
├── gradlew.bat
└── settings.gradle

这里直接在根目录下创建 buildSrc 目录,如果采用 Java 开发插件,则前面三级目录为 src/main/java ,如果是 Groovy 则目录为 src/main/groovy ,kotlin 同理;剩下的则为插件包名可以自行定义。开发插件重要的有两点,这里我们以 Java 为示例:

1. buildSrc/build.gradle :

在 buildSrc 目录下创建 build.gradle,内容如下:

plugins {
id 'java-gradle-plugin' // ①
}

gradlePlugin {
plugins {
greeting { // ②
id = 'com.zsk.plugin' // ③
implementationClass = 'com.zsk.plugin.MyPlugin' // ④
}
}
}
  • ① : 用于在 Gradle 构建环境中支持 Java 插件开发的插件
  • ② : 给插件定义名称
  • ③ : 插件唯一标识符,用于插件引用,通常采用 groupId.artifactId 格式
  • ④ : 实现插件逻辑的Java类的全限定名(包括包名和类名),Gradle会使用这个类来实例化插件,并调用其方法来执行插件的功能

2. 插件实现类

package com.zsk.plugin;  

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Action;

public class MyPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("这是我的第一个插件!");

project.getTasks().create("MyTask", task -> {
task.doLast(task1 -> System.out.println("插件任务 MyTask 执行!"));
});
}
}

定义 MyPlugin 类实现 Plugin 接口,并在插件中定义了一个 MyTask 的任务,在根目录下执行 ./gradlew MyTask 命令:

> Configure project :
这是我的第一个插件!

> Task :MyTask
插件任务 MyTask 执行!

上面是将 MyTask 直接定义在插件类中,我们可以在 buildSrc 目录中单独定义任务类 MyTask2,然后在 Plugin 类中直接引用:

package com.zsk.plugin;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

public class MyTask2 extends DefaultTask {
@TaskAction
void taskAction() {
//taskAction名字可自定义
System.out.println("插件任务 MyTask2 执行!");
}
}

在 MyPlugin 直接引用:

public class MyPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {

project.getTasks().create("MyTask2", MyTask2.class);
}
}

3.3 Standalone project

buildSrc 的形式限定了只能在本工程中使用,而其他的项目工程不能使用,因此我们可以将插件移动至独立项目,以便发布并与他人共享。这项目只是一个 Java 项目,生成包含插件类的 Jar,打包和发布可以利用 Gradle 中内置的插件: java-gradle-plugin 。这里我们还是脱离其他平台,而只使用 Gradle 本身的命令来实现。其实其他平台例如 Android Studio 在创建发布插件的流程和发布一个普通的 Jar 包过程完全一致。

  1. 新建目录 standalonePlugin,并准备 gradle 的执行环境
├── gradle
├── gradlew
  1. 根目录下执行如下指令,其中选项可根据实际自行配置
./gradlew init --type java-gradle-plugin

这样一个独立的插件已经创建完成!目录结构如下:

├── gradle
├── gradlew
├── gradlew.bat
├── plugin
└── settings.gradle

其中已经自动生成了一个插件类以及一个默认的任务:

package cn.zsk.plugin;

import org.gradle.api.Project;
import org.gradle.api.Plugin;

/**
* A simple 'hello world' plugin.
*/
public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {
// Register a task
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println("Hello from plugin 'cn.zsk.plugin.greeting'"));
});
}
}

现在已经有了这样的一个插件工程,接下来要做的就是将这个工程上传至 Maven 以便分享给其他人使用,现在来修改 plugin/build.gradle :


plugins {
id 'java-gradle-plugin'
id 'maven-publish' // ① 配置 Maven 插件
}

......

gradlePlugin {
// Define the plugin
plugins {
greeting {
id = 'cn.zsk.plugin'
implementationClass = 'cn.zsk.plugin.MyPlugin'
}
}
}

publishing { // ② 配置 Maven 基础信息
publications {
maven(MavenPublication) {
groupId = 'cn.zsk.plugin'
artifactId = 'MyPlugin'
version = '1.1.0'

from components.java
}
}
repositories {
maven {
url = uri('http://xxx.xxx.xxx-xxx.com/xxx/maven-releases/')

credentials {
username = "xxx"
password = "xxx"
}
// 如果你的仓库要求使用HTTPS,并且你的Gradle版本低于7.3,你可能需要设置这个
// allowInsecureProtocol = true // 注意:出于安全考虑,不建议在生产环境中使用
allowInsecureProtocol true
}
}
}
......

在根目录下执行如下指令:

./gradlew publishMavenPublicationToMavenRepository

具体为什么是这个指令,目前猜测是和 publishing 模块中的 MavenPublication 属性有关系,若是直接使用 ./gradlew publish 会有额外的信息提交到 Maven 仓库上。留到后续在补充。

  1. 如何使用

上面我们已经将插件上传至 Maven 仓库中了,接下来我们演示如何使用这个插件:

1、在 setting.gradle 中配置仓库地址:

pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven {
url 'http://xxx.xxx.xxxx.com/repository/maven-public/'
allowInsecureProtocol = true
}
}
}

2、在根目录下的 build.gradle 中生命插件的类路径:

buildscript {
dependencies {
classpath 'cn.zsk.plugin:MyPlugin:1.1.0'
}
}

3、在 app/build.gradle 中应用插件:

plugins {
...
id 'cn.zsk.plugin'
}

4、验证插件:

//根目录下执行 ./gradlew greeting 可得:

> Task :app:greeting
Hello from plugin 'cn.zsk.plugin.greeting'

3.4 插件扩展

假如我们希望在插件运行的时候响应外界的输入,那就使得插件具有更高的灵活性。这就需要用到 Extension 属性来实现。这里还是在 3.3 小节中创建的插件工程上来添加扩展。

首先建立一个实体类,用于接收参数:

package cn.zsk.plugin;

public class MyExtension {
String pluginName;
int version;
}

在插件类中添加扩展

package cn.zsk.plugin;

import org.gradle.api.Project;
import org.gradle.api.Plugin;

public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {

//添加扩展
project.extensions.add('myExtension',MyExtension);

// Register a task
project.getTasks().register("showExtension", task -> {
task.doLast(s -> {
MyExtension extension = (MyExtension) project.getExtensions().getByName("myExtension");
System.out.println("Extension : pluginName = "+extension.pluginName +" version = "+extension.version);
});
});
}
}

修改一下版本重新上传 Maven 仓库,之后就可以在 module 中使用了:

myExtension {
pluginName = "zskPlugin"
version = 3
}

注意这里是采用 = 好来为扩展插件来赋值,这和常见的 Android 中的 android{…} 块不太一样,是省略 “=” 号,如下所示:

android {
namespace 'com.example.jnidemo'
compileSdk 34

defaultConfig {
applicationId "com.example.jnidemo"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}

...
}

Android Gradle Plugin(AGP)是 Gradle 的一个特殊插件,它专门为Android项目的构建和管理而设计。AGP 引入了许多自定义的任务、类型和扩展,这些都是为了简化 Android 应用的构建过程。在 AGP中,一些配置属性(如 compileSdkVersion、minSdkVersio n等)允许省略等号(=)进行配置,这是因为在 AG P的 DSL 设计中,这些属性被特别处理以支持更简洁的语法。

这种语法糖(syntactic sugar)是 AGP 团队为了提升Android开发者体验而引入的,并不是 Gradle 本身的标准特性。它利用了 Groovy 的灵活性和Gradle DSL 的扩展性,但在其他非 Android 的 Gradle 插件中并不常见,对于一般的 Gradle 插件,Gradle 没有提供类似的语法糖来自动省略等号。因此,在配置这些插件的扩展时,需要遵循 Gradle 的标准 DSL 语法,即使用 = 来赋值。

在上面 android {…} 块中存在嵌套扩展模块 defaultConfig {…},那么这种应该如何实现呢?

① 定义一个扩展的功能类

package cn.zsk.plugin;

public class DefaultConfig {
String id;
}

② 添加到要嵌套的扩展类中国

package cn.zsk.plugin;

import org.gradle.api.Action;
import org.gradle.util.internal.ConfigureUtil;
import groovy.lang.Closure;

public class MyExtension {
String pluginName;
int version;
DefaultConfig defaultConfig = new DefaultConfig();

// 方式一: 参数为Action
void setDefaultConfig(Action<DefaultConfig> action) {
action.execute(defaultConfig);
}

// 方式二: 闭包形式
void setDefaultConfig(Closure config) {
ConfigureUtil.configure(config, defaultConfig);
}

// 方式三: 直接使用属性名作为方法名来对属性进行赋值
void defaultConfig(Action<DefaultConfig> action) {
action.execute(defaultConfig);
}
}

③ 在插件中应用

package cn.zsk.plugin;

import org.gradle.api.Project;
import org.gradle.api.Plugin;

public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {

//添加扩展
project.getExtensions().create("myExtension", MyExtension.class);

// Register a task
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println("Hello from plugin 'cn.zsk.plugin.greeting'"));
});

project.getTasks().register("showExtension", task -> {
task.doLast(s -> {
MyExtension extension = (MyExtension) project.getExtensions().getByName("myExtension");
System.out.println("Extension : pluginName = "+extension.pluginName +" version = "+extension.version+" id = "+extension.defaultConfig.id);
});
});
}
}

这样重新上传 Maven 或者以 module 的形式 include 到项目中时,在 build.gradle 中可如下配置:


// 方式一: 参数为Action;
myExtension {
pluginName = "zskPLugin"
version = 3
defaultConfig {
it.id = "1.2.3"
}
}

//方式二:闭包形式; 方式三:直接使用属性名作为方法名来对属性进行赋值;
myExtension {
pluginName = "zskPLugin"
version = 3
defaultConfig {
id = "1.2.3"
}
}

执行 ./gradlew showExtension 任务可得如下结果:

> Task :app:showExtension
Extension : pluginName = zskPLugin version = 3 id = 1.2.3

好了我们可以来考虑另外一种情况,比如在 Android 打包 apk 时,在 build.gradle 中配置的 buildTypes 块,在这个块中,我们可以自定义扩展的属性名如比,release、debug、other… 这是怎么做到的呢? 这就不得不提 NamedDomainObjectContainer 这个属性了,这里借用这个属性来为我们的插件扩展出自定义属性名的功能。

① 定义 BuildTypes 类:

package cn.zsk.plugin;

public class BuildTypes {
// 必须有 String name 属性,且不允许构造后修改
private String name;

// 业务参数
private boolean signingConfig;

// 必须带有以 String name 为参数的 public 构造函数
public BuildTypes(String name) {
this.name = name;
}

public String getName() {
return name;
}

public boolean isSigningConfig() {
return signingConfig;
}

public void setSigningConfig(boolean signingConfig) {
this.signingConfig = signingConfig;
}
}

这里有两个必须要注意的点:

  • 必须有 String name 属性,且不允许构造后修改
  • 必须带有以 String name 为参数的 public 构造函数

② 将 BuildTypes 添加到扩展类中

package cn.zsk.plugin;

import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.util.internal.ConfigureUtil;
import groovy.lang.Closure;
import org.gradle.api.Project;

public class MyExtension {
String pluginName;
int version;
DefaultConfig defaultConfig = new DefaultConfig();

// 添加 BuildTypes 类型
NamedDomainObjectContainer<BuildTypes> buildTypes;

public MyExtension(Project project) {
// 创建 NamedDomainObjectContainer
NamedDomainObjectContainer<BuildTypes> buildTypeObjs = project.container(BuildTypes.class);
buildTypes = buildTypeObjs;
}

// 选用闭包形式形式,也可用
void setBuildTypes(Closure config){
ConfigureUtil.configure(config, buildTypes);
}

public NamedDomainObjectContainer<BuildTypes> getBuildTypes() {
return buildTypes;
}

// 方式一: 参数为Action
void setDefaultConfig(Action<DefaultConfig> action) {
action.execute(defaultConfig);
}

// 方式二: 闭包形式
void setDefaultConfig(Closure config) {
ConfigureUtil.configure(config, defaultConfig);
}

// 方式三: 直接使用属性名作为方法名来对属性进行赋值
void defaultConfig(Action<DefaultConfig> action) {
action.execute(defaultConfig);
}
}

③ 在插件类中使用方式

package cn.zsk.plugin;

import org.gradle.api.Project;
import org.gradle.api.Plugin;
import java.util.List;

public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {

//添加扩展
project.getExtensions().create("myExtension", MyExtension.class);

// Register a task
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println("Hello from plugin 'cn.zsk.plugin.greeting'"));
});

project.getTasks().register("showExtension", task -> {
task.doLast(s -> {
MyExtension extension = (MyExtension) project.getExtensions().getByName("myExtension");
System.out.println("Extension : pluginName = " + extension.pluginName + " version = " + extension.version + " id = " + extension.defaultConfig.id);

//打印所有 BuildTypes
List<BuildTypes> buildTypes = extension.getBuildTypes().stream().toList();
for (BuildTypes types : buildTypes) {
System.out.println("BuildTypes : "+types.getName() +" isSigningConfig : "+types.isSigningConfig());
}
});
});
}
}

这样当将此插件在项目中应用之后,在 build.gradle 中配置如下:

myExtension {
pluginName = "zskPLugin"
version = 3
defaultConfig {
id = "1.2.3"
}
buildTypes {
dev {
signingConfig = false
}
release {
signingConfig = true
}
}
}

这样当执行 ./gradlew showExtension 任务时,打印信息如下:

> Task :app:showExtension
Extension : pluginName = zskPLugin version = 3 id = 1.2.3
BuildTypes : dev isSigningConfig : false
BuildTypes : release isSigningConfig : true

四、Plugins DSL

Plugins DSL 是一种专门用于配置和引用 Gradle 插件的领域特定语言。它提供了一种简介的方式来应用插件。上面我们引用插件时是采用旧的方式,需要显式的添加 classpath 等,那么当访问核心插件或者社区插件时,通过 Plugins DSL 就可以大大的简化配置过程:

① 核心插件:

plugins {
id 'java'
}

② 社区插件

plugins {
//apply false 语法告诉 Gradle 不要将插件应用于当前项目
id 'com.jfrog.bintray' version '1.8.5' apply false
}

注意 Plugins DSL 方式,仅限于 Gradle 的核心插件或者上传到公共社区的插件,普通的自定义的插件只能用我们上面的旧的方式,更多可见官网Plugins DSL介绍。

//