Gradle之自定义插件的三种方式

Android Gradle插件中,包含了一些task可以帮我们做一些编译、引入依赖、打包等工作,比如assembleBuild,clean等等。可以使用多种语言来实现Gradle插件,其实只要最终被编译为JVM字节码的都可以,常用的有Groovy、Java、Kotlin。

自定义gradle插件的官方网址

比如在模块的build.gradle下需要引入的插件,这两个插件就是两个java程序。

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}
什么是 Gradle 插件

Gradle 和 Gradle 插件是两个完全不同的概念,Gradle 提供的是一套核心的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,本质上和 .gradle 文件是相同。例如,我们熟悉的编译 Java 代码的能力,都是由插件提供的。

Gradle 插件的优点

Gradle 插件使用了独立模块封装构建逻辑,无论是从开发开始使用来看,Gradle 插件的整体体验都更友好。

1.逻辑复用: 将相同的逻辑提供给多个相似项目复用,减少重复维护类似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好;
2.组件发布: 可以将插件发布到 Maven 仓库进行管理,其他项目可以使用插件 ID 依赖。当然 .gradle 文件也可以放到一个远程路径被其他项目引用;
3.构建配置: Gradle 插件可以声明插件扩展来暴露可配置的属性,提供定制化能力。当然 .gradle 文件也可以做到,但实现会麻烦些。

Gradle 插件的核心类是 Plugin,一般使用 Project 作为泛型实参。当使用方引入插件后,其实就是调用了 Plugin#apply() 方法,我们可以把 apply() 方法理解为插件的执行入口。例如:

public class MyPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("This is my plugin");
    }
}
应用插件的步骤

1、将插件添加到 classpath: 将插件添加到构建脚本的 classpath 中,我们的 Gradle 构建脚本才能应用插件。这里区分本地依赖和远程依赖两种情况。
本地依赖: 指直接依赖本地插件源码,一般在调试插件的阶段是使用本地依赖的方式。例如:

buildscript {
    ...
    dependencies {
        // For Debug
        classpath project(":myplugin")
    }
}

远程依赖: 指依赖已发布到 Maven 仓库的插件,一般我们都是用这种方式依赖官方或第三方实现的 Gradle 插件。例如:

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30"
    }
    ...
}

2、使用 apply 应用插件: 在需要使用插件的 .gradle 脚本中使用 apply 应用插件,这将创建一个新的 Plugin 实例,并执行 Plugin#apply() 方法。例如:

apply plugin: 'com.android.application'
// 或者
plugins {
    id 'com.android.application'
}
开发Gradle插件有3种方式,如下:

以下示例Demo Github下载地址:

CreateGradlePlugin

1.直接引用插件

创建java模块,写一个自定义Plugin

public class MyPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("This is my plugin");
    }
}

在引入模块下比如app模块下,引用该Plugin

import com.github.buildsrc.MyPlugin

apply plugin: MyPlugin
2.通过特殊的 buildSrc 模块写插件

插件模块的名称是任意的,除非使用了一个特殊的名称 “buildSrc”,buildSrc 模块是 Gradle 默认的插件模块。buildSrc 模块本质上和普通的插件模块是一样的,有一些小区别:

1、buildSrc 模块会被自动识别为参与构建的模块,因此不需要在 settings.gradle 中使用 include 引入,就算引入了也会编译出错
2、buildSrc 模块会自动被添加到构建脚本的 classpath 中,不需要手动添加
3、buildSrc 模块的 build.gradle 执行时机早于其他 Project

开发Gradle插件入门示例:

使用buildSrc目录方法。

1.在项目下创建Directory,命名为buildSrc,然后创建目录src/main/java。

2.同步一下项目,会在该目录下生成build目录。


3.在java目录下创建包,并创建一个自己的插件类,比如命名BuildSrcPlugin。

class BuildSrcPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
            System.out.println("This is buildSrc plugin");
    }
}

在buildSrc目录下写代码特殊性:
1.它是提供给各个模块guild.gradle使用的
2.这个模块默认会有gradle所需库
3.这个模块不需要setting.gradle去引用

在模块 build.gradle 文件中增加以下配置,gradlePlugin 定义了插件 ID 和插件实现类的映射关系:

gradlePlugin {
    plugins {
        buildsrc {
            // Plugin id.
            id = 'com.github.buildsrc'
            // Plugin implementation.
            implementationClass = 'com.github.buildsrc.BuildSrcPlugin'
        }
    }
}

这其实是 Java Gradle Plugin 提供的一个简化 API,其背后会自动帮我们创建一个 [插件ID].properties 配置文件,Gradle 就是通过这个文件类进行匹配的。如果你不使用 gradlePlugin API,直接手动创建 [插件ID].properties 文件,作用是完全一样的。


properties内容:

implementation-class=com.github.customplugin.CustomPlugin

4.使用该插件,在需要引入插件的build文件中引入:

plugins {
    id 'com.github.buildsrc'  //引用buildSrc下声明的插件
}

在执行build文件时,执行该行代码,会加载该类,去执行BuildSrcPlugin的apply方法。

打印apply方法里的文本:

3.独立模块发布插件

1.新建Java module


2.在build.gradle中应用gradle插件:

plugins {
    id 'java-gradle-plugin'
}

写一个Plugin类

public class CustomPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("This is a custom plugin");
    }
}

声明一个插件id以及对应Plugin类:

gradlePlugin {
    plugins {
        customplugin {
            // Plugin id.
            id = 'com.github.customplugin'
            // Plugin implementation.
            implementationClass = 'com.github.customplugin.CustomPlugin'
        }
    }
}

定义发布代码到本地maven-push配置:

apply plugin: 'maven-publish'
publishing {
    publications{
        maven(MavenPublication) {
            groupId "com.github.custom"
            artifactId 'CustomPlugin'
            version "1.0.0"
            //如果是war包填写components.web,如果是jar包填写components.java
            from components.java
        }
    }

    repositories {
        maven {
            url = "../repo"
        }
    }
}

在task任务中使用pushing发布jar包到本地:


在本地生成jar包仓库:


添加该插件classpath:

buildscript {
    dependencies {
        classpath "com.github.custom:CustomPlugin:1.0.0"
    }
}

在模块级 build.gradle 文件中 apply 插件:

plugins {
    id 'com.github.customplugin'
}

CustomPlugin的apply方法执行:


Screenshot 2023-06-05 at 14.34.28.png

我们能做哪些gradle插件

  • Apk防破解插件:
    利用破解之后打包的签名与我们自己的包签名不相同来处理,这里RuntimeException的抛出也可以用字节码插桩,将崩溃处理的代码插到启动activity里,让破解的包无法再正常运行。
public class AntiCrackPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        TaskProvider<Jar> jarTask = project.getTasks().named("jar", Jar.class);

        // 在打包任务之前执行防破解逻辑
        jarTask.configure(task -> {
            task.doFirst("AntiCrack", t -> {
                System.out.println("Running anti-crack checks...");

                // 添加你的防破解逻辑,例如检查签名、防止二次打包等
                // 这里只是一个简单的示例,实际应用需要更加复杂的逻辑
                if (isCracked(project)) {
                    throw new RuntimeException("The application has been cracked!");
                }
            });
        });
    }

    private boolean isCracked(Project project) {
        // 添加你的防破解逻辑,例如检查签名、防止二次打包等
        // 这里只是一个简单的示例,实际应用需要更加复杂的逻辑

        // 获取应用的签名信息
        String expectedSignature = "your_expected_signature"; // 期望的签名信息,可以从安全的渠道获取

        try {
            JarFile jarFile = new JarFile(project.getTasks().getByPath("jar").getOutputs().getFiles().getSingleFile());
            Certificate[] certificates = jarFile.getJarEntry("META-INF/MANIFEST.MF").getCodeSigners()[0].getSignerCertPath().getCertificates();
            StringBuilder actualSignature = new StringBuilder();
            for (Certificate certificate : certificates) {
                actualSignature.append(certificate.getPublicKey().toString());
            }

            // 比较实际签名和期望签名
            return !actualSignature.toString().equals(expectedSignature);
        } catch (Exception e) {
            e.printStackTrace();
            return true; // 发生异常时可能是被篡改过的APK,视为破解
        }
    }
}

  • 隐私合规处理的gradle插件
    在Android应用中处理隐私合规性是非常重要的,特别是在涉及用户隐私信息的处理方面。首先,让我们考虑一些隐私合规性的基本要求:

权限检查: 确保应用只请求和使用了必要的权限。
隐私政策链接: 确保应用中包含隐私政策链接,并提供用户访问的方式。
数据收集通知: 如果应用收集用户数据,确保提供了适当的通知和用户同意。

我们可以创建一个Gradle插件,执行这些检查:

public class PrivacyCompliancePlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        TaskProvider<PrivacyComplianceCheckTask> privacyCheckTask = project.getTasks().register("privacyCheck", PrivacyComplianceCheckTask.class);

        project.afterEvaluate(p -> {
            // 在构建前执行隐私合规性检查
            project.getTasks().getByName("assemble").dependsOn(privacyCheckTask);
        });
    }
}

然后,创建一个任务用于执行实际的隐私合规性检查:

public class PrivacyComplianceCheckTask extends DefaultTask {

    @TaskAction
    public void checkPrivacyCompliance() {
        // 添加你的隐私合规性检查逻辑
        checkPermissions();
        checkPrivacyPolicyLink();
        checkDataCollectionNotice();
    }

    private void checkPermissions() {
    System.out.println("Checking permissions...");

    List<String> requiredPermissions = Arrays.asList(
            "android.permission.CAMERA",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            // 添加其他需要的权限
    );

    Set<String> missingPermissions = new HashSet<>(requiredPermissions);
    missingPermissions.removeAll(getDeclaredPermissions());

    if (!missingPermissions.isEmpty()) {
        throw new RuntimeException("Missing required permissions: " + missingPermissions);
    }
}

private Set<String> getDeclaredPermissions() {
    AndroidManifest androidManifest = new AndroidManifest(getProject().file("src/main/AndroidManifest.xml"));
    return androidManifest.getPermissions();
}

private void checkPrivacyPolicyLink() {
    System.out.println("Checking privacy policy link...");

    String privacyPolicyLink = getPrivacyPolicyLink();
    
    if (privacyPolicyLink == null || privacyPolicyLink.isEmpty()) {
        throw new RuntimeException("Privacy policy link is missing or empty.");
    }
}

private String getPrivacyPolicyLink() {
    // 在这里,你可以通过读取资源文件或其他配置方式获取隐私政策链接
    return "https://www.example.com/privacy-policy";
}

   private void checkDataCollectionNotice() {
    System.out.println("Checking data collection notice...");

    boolean dataCollectionEnabled = isDataCollectionEnabled();
    
    if (!dataCollectionEnabled) {
        throw new RuntimeException("Data collection is not enabled.");
    }
}

private boolean isDataCollectionEnabled() {
    // 在这里,你可以通过读取应用的配置文件或其他方式获取数据收集的状态
    return true; // 假设数据收集是启用的
}
}

参考:
https://blog.csdn.net/wumeixinjiazu/article/details/124691763
https://segmentfault.com/a/1190000041856822

Demo地址:

https://github.com/running-libo/CreateGradlePlugin

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容