Gradle插件 protobuf自动编译

1,709 阅读13分钟

前言

protobufGoogle推出的序列化协议,比json所占的字节更小,序列化更快等特点。本文简单介绍协议特别点然后介绍如何编写一个Gradle插件实现自动编译。

protobuf Github地址
protobuf 语法教程

一个直观的例子:

  val protoBufPerson = AddressBookProtos
            .Person
            .newBuilder()
            .setEmail("xxx@qq.com")
            .setId(23)
            .setName("王五").build()
        val jsonPerson = """{"email":"xxx@qq.com","id":23,"name":"王五"}"""

        Log.e("MainActivity", "protoBufPerson :${protoBufPerson.serializedSize}  jsonPerson :${jsonPerson.toByteArray().size}")

输出:

 protoBufPerson :22  jsonPerson :46

可见protobuf序列化后是json的数据体的一半左右。具体性能相关可以参考官网。

Tip:移动端可以考虑用lite版本减少生成的类体积

protobuf 使用流程

由于官网有详细的介绍,这里只做简单描述。

1 编辑proto文件

syntax = "proto2";

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

}

proto语法是跨语言的,所以我们需要需要对应平台的编译器工具,编译成java文件,比如笔者在MacOs下,需要下载这个平台的编译工具。

2 执行编译工具,将中间语法文件编译成java文件:

protoc --java_out=输出目录  编译文件   -I=编译文件所在的文件夹

3 拷贝生成java文件到工程

为了提高开发效率,大神们早就推出了gradle插件帮我们完成自动编译后自动加入工程目录。

如下将proto文件放入目录即可 直接使用Person.proto编译后产物
在这里插入图片描述

开始编写内嵌插件

gradle如果仅仅为本工程使用,可以在当前目录创建一个特殊目录buildSrc,然后在buildSrc目录放置build.gradle文件即可,如果你插件需要额外的配置可以自行在build.gradle添加依赖。Developing Custom Gradle Plugins.

本例在一个Android工程目录下做演示:

├── AndroidProtpPlugin.iml
├── app
│   ├── build.gradle
│   ├── libs
│   ├── proguard-rules.pro
│   └── src
├── build.gradle
├── buildSrc
│   └── build.gradle
├── gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle

基础类编写

首先编辑一个插件类,实现Plugin接口即可

//MyProtoPlugin.java
public class MyProtoPlugin implements Plugin<Project> {

    //gradle 日志输出工具。参数表示是否输出日志,false表示输出
    Logger logger = new Logger(false);

	//插件被应用的时候回调
    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin 插件被激活");
    }
}

然后在app模块下的build.gradle下应用

//build.gradle
apply plugin:MyProtoPlugin

运行./gradlew :app:help

输出:

./gradlew -q :app:help
MyProtoPlugin 插件被激活

获取用户配置proto文件目录

我们插件需要用户告诉需要编译的proto文件位置,所以我们暴露一个DSL给开发者自定义。

public class MyProtoPlugin implements Plugin<Project> {

    //gradle 日志输出工具。参数表示是否输出日志,false表示输出
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin 插件被激活");

        /**
         * 创建一个插件DSL扩展,第一个参数为DSL名字,第二个为配置类
         *  比如:
         *     protoConfig{
         *             // protoDirPath为MyProtoConfigExtension内部属性
         *             protoDirPath = "张三"
         *
         *     }
         */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);


        //等project配置阶段完毕输出 用户配置
        project.afterEvaluate(project1 -> {
            MyProtoConfigExtension configExtension = project1.getExtensions().getByType(MyProtoConfigExtension.class);
            logger.log("用户配置的目录" + configExtension.protoDirPath);
        });
    }
}
public class MyProtoConfigExtension {
	//要编译的proto文件目录
    String protoDirPath;

    public String getProtoDirPath() {
        return protoDirPath;
    }

    public void setProtoDirPath(String protoDirPath) {
        this.protoDirPath = protoDirPath;
    }
}

插件应用处修改代码

//build.gradle
apply plugin: MyProtoPlugin
//配置要编译protobuf文件位置
protoConfig {
    protoDirPath = "src/main/proto"
}

获取用户当前系统对应proto编译器

有的开发者使用Mac,或者linux,我们需要根据此选择一个对应版本的编译器去编译proto文件。
Google发布了一个artifact: com.google.protobuf:protoc
我们看工件(artifact)目录:
在这里插入图片描述
我们可以看到这个工件中有很多的编译器版本。但是假设我们只想拿mac的编译器怎么办?我们可以看下protoc-xxx.pom文件
我们可以看到pom提供类分类器(classifier)供我们选择.(classifier是maven的基础知识,不清楚的同学可以看下网上轮子,可以简单理解一个artifact具有 组织名,工件名称,版本号,可选分类器,可选文件后缀ext等)
在这里插入图片描述
protoc文件目录

比如我们需要mac下的编译器,依赖写法如下

dependencies {
    implementation group: 'com.google.protobuf', name: 'protoc', version: '3.14.0',classifier:'osx-x86_64',ext:'exe'
}

上述我们知道了如何从artifact取出对应平台的编译器,那么我们如何判断当前gradle执行环境的操作系统呢?我们可以使用google提供的插件帮助我们

osdetector-gradle-plugin

我们给插件目录buildSrc下的build.gradle添加依赖

repositories {
    maven {
        url 'https://mirrors.huaweicloud.com/repository/maven/'
    }
    google()
    jcenter()
}
dependencies {
    implementation 'com.google.gradle:osdetector-gradle-plugin:1.6.2'
}

继续编辑插件


public class MyProtoPlugin implements Plugin<Project> {

    //gradle 日志输出工具。参数表示是否输出日志,false表示输出
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
         //...
        //利用osdetector得到对应系统分类器
        OsDetector osDetector = new OsDetector();
        logger.log("当前操作系统的分类器 " +osDetector.getClassifier());
		//...
    }
}

输出

当前操作系统的分类器 osx-x86_64

我们最后获取protoc编译器maven即可


		OsDetector osDetector = new OsDetector();
        logger.log("当前操作系统的分类器 " + osDetector.getClassifier());


        //这个名字用于创建一个configuration,configuration可以简单理解为管理一组依赖的管理器
        //implementation 和testImplementation就是其中一个管理器
        String MycName = "customPluginConfiguration";
        //创建一个管理器名字为customPluginConfiguration
        Configuration configuration = project.getConfigurations().create(MycName);

        //这个是构造proto的artifact的信息,这些信息可以查看pom文件得知
        HashMap<String, String> protocArtifactMap = new HashMap<>();
        protocArtifactMap.put("group", "com.google.protobuf");
        protocArtifactMap.put("name", "protoc");
        protocArtifactMap.put("version", "3.14.0");
        protocArtifactMap.put("classifier", osDetector.getClassifier());
        protocArtifactMap.put("ext", "exe");

        //添加依赖到MycName这个管理器中
        Dependency protoDependency = project.getDependencies().add(MycName, protocArtifactMap);

        //用管理器返回这个依赖的所有的文件
        FileCollection files = configuration.fileCollection(protoDependency);
        //因为这个依赖只会存在一个文件也就是编译器
        File protoExe = files.getSingleFile();
        logger.log("获得的平台编译器 " + protoExe.getAbsolutePath());

执行编译protoc文件

上面我们得到信息:

  1. 本地平台的protoc编译器,如果mac版本编译器
  2. 工程proto文件路径

我们编写一个专门编译和输出结果的Task

//CompilerProtoTask.java
public class CompilerProtoTask extends DefaultTask {

    Logger logger = Logging.getLogger(CompilerProtoTask.class);

    @Input
    String protoDir;

	//输出编译后的文件夹
    @OutputDirectory
    String outGeneratedDir;

    {
        outGeneratedDir = getProject().getBuildDir() + "/generated/source/protos/";
    }

    @TaskAction
    void action() {

        OsDetector osDetector = new OsDetector();


        //这个名字用于创建一个configuration,configuration可以简单理解为管理一组依赖的管理器
        String MycName = "customPluginConfiguration";
        //创建一个管理器名字为customPluginConfiguration
        Configuration configuration = getProject().getConfigurations().create(MycName);

        //这个是构造proto的artifact的信息,这些信息可以查看pom文件得知
        HashMap<String, String> protocArtifactMap = new HashMap<>();
        protocArtifactMap.put("group", "com.google.protobuf");
        protocArtifactMap.put("name", "protoc");
        protocArtifactMap.put("version", "3.14.0");
        protocArtifactMap.put("classifier", osDetector.getClassifier());
        protocArtifactMap.put("ext", "exe");

        //添加依赖到MycName这个管理器中
        Dependency protoDependency = getProject().getDependencies().add(MycName, protocArtifactMap);

        //用管理器返回这个依赖的所有的文件
        FileCollection files = configuration.fileCollection(protoDependency);
        //因为这个依赖只会存在一个文件也就是编译器
        File protoExe = files.getFiles().stream().findFirst().get();


        try {
            //获得扩展类对象实例,主要用于获取用户配置的proto文件路径
            String protoDirPath = protoDir;

            File file1 = new File(getProject().getProjectDir(), protoDirPath);
            //得到用户配置proto文件目录下的所有后缀为proto的文件
            String[] extensionFilter = {"proto"};
            Collection<File> protoDifFile = FileUtils.listFiles(new File(getProject().getProjectDir(), protoDirPath), extensionFilter, false);

            //拼接命令行字符串
            StringBuilder cmd = new StringBuilder(protoExe.getAbsolutePath() + " ");

            File outFile = new File(outGeneratedDir);
            if (!outFile.exists()) {
                outFile.mkdirs();
            }
            cmd.append("--java_out=" + outGeneratedDir);
            for (File file : protoDifFile) {
                String replaceFilePath = " " + file.getPath().replaceFirst(file1.getAbsolutePath() + "/", "") + " ";
                cmd.append(replaceFilePath);
            }
            cmd.append(" -I" + protoDirPath + " ");

            logger.info("运行编译命令 " + cmd);
            //防止编译器无权限运行
            if (!protoExe.canExecute() && !protoExe.setExecutable(true)) {
                throw new GradleException("protoc编译器无法执行");
            }


            //执行命令行
            Process exec = null;
            try {
                String[] strings = new String[0];
                exec = Runtime.getRuntime().exec(cmd.toString(), strings, getProject().getProjectDir());
                int resultCode = exec.waitFor();

                //执行成功
                if (resultCode == 0) {

                } else {
                    throw new GradleException("编译proto文件错误" + IOUtils.toString(exec.getErrorStream()));
                }
            } finally {
                if (exec != null) {
                    exec.destroy();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

简单来说上面的Task就是得到编译器然后执行shell命令编译出源文件.

我们将上面的Task 整合到插件中

public class MyProtoPlugin implements Plugin<Project> {

    //gradle 日志输出工具。参数表示是否输出日志,false表示输出
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin 插件被激活");

        /**
         * 创建一个插件DSL扩展,第一个参数为DSL名字,第二个为配置类
         *  比如:
         *     protoConfig{
         *             // protoDirPath为MyProtoConfigExtension内部属性
         *             protoDirPath = "张三"
         *
         *     }
         */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);

		//在evaluate之后添加task到gradle中
        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {

                /**
                 * 创建任务并设置输出目录
                 */
                CompilerProtoTask compilerProto = project.getTasks().create("compilerProto", CompilerProtoTask.class);
//                compilerProto.onlyIf(new );
                compilerProto.setGroup("proto");
                MyProtoConfigExtension myProtoConfigExtension = project.getExtensions().getByType(MyProtoConfigExtension.class);
                compilerProto.protoDir = myProtoConfigExtension.protoDirPath;
            }
        });
    }
}    

执行如下命令会在build/granerated/source/protos生成java文件

./gradlew compilerProto

在这里插入图片描述
虽然插件生成源文件,但是java编译器不知道这个文件要被打包到工程中。比如生成了A.java,我们想要gradle自动帮我编译这个文件并最后打包到jar或者apk中.

关联插件生成类到编译路径中

我们上面生成了类文件,但是还没有关联到编译路径中,也就是我们生成的类不会打包到Jar中或者Android的apk中.

我们需要知道当前是java工程还是Android工程.

Android工程处理源码关联

对于Android的工程来说会引用AGP无非两种:

  1. apply plugin: 'com.android.application'
  2. apply plugin: 'com.android.library'

前者是Apk应用,后者是Android 类库.

我们在插件buildSrc下的build.gradle添加Android插件依赖:

dependencies {
	//...
    implementation  'com.android.tools.build:gradle:4.2.0-alpha16'
	//..
}

于是乎我们判断应用插件的工程是否为Android的逻辑如下:

 //是Android工程
    boolean isAndroidProject(Project project) {
        return project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class) || project.getPlugins().hasPlugin(com.android.build.gradle.LibraryPlugin.class);
    }

apply plugin: 'com.android.application'应用的是AppPlugin
apply plugin: 'com.android.library'应用的是LibraryPlugin

关于为什么一个字符串可以关联到某个插件类,我们这里简单讲下,插件开发者长传的时候会配置如下内容到gradle中

gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.samples.greeting"
            implementationClass = "org.gradle.GreetingPlugin"
        }
    }
}

这个插件配置会在发布的时候生成一个数据位于
src / main / resources / META-INF / gradle-plugins / org.samples.greeting.properties
内容如下

implementation-class=org.gradle.GreetingPlugin

上面我们知道如何判断Android工程和Java工程(不是Android工程那就是Java工程),但是还没告诉Android插件将我们的生成proto目录加入编译路径中,但是AGP提供了对应的函数方便我们操作。

class BaseVariant{
	void registerJavaGeneratingTask(@NonNull Task task, @NonNull File... sourceFolders);
}

本文为了防止读者读Android Gradle插件不熟悉这里解释说明下构建变体和风味:

如下案例:

android{
 //构建类型
 buildTypes {
        release {
           
        }
        debug {

        }

    }
    //风味维度
    flavorDimensions "version", "hha"
	//产品风味
    productFlavors {
        demo {
            dimension "version"
            applicationIdSuffix ".demo"
            versionNameSuffix "-demo"
        }
        full {
            dimension "version"
            applicationIdSuffix ".full"
            versionNameSuffix "-full"
        }
        nnimei {
            dimension "hha"
            applicationIdSuffix ".full"
            versionNameSuffix "-full"
        }
    }
}

上面会将不同维度的产品风味组合在一起,最后组合到构建类型。

如上案例:两个维度version, hha组合产品风味,就有两种结果分别为:

1. fullnnimei
2. demonnimei

结合构建类型

1.fullnnimeiDebug
2.demonnimeiDebug
3.fullnnimeiRelease
4.demonnimeiRelease

这样便生成了四种最终变体。
在这里插入图片描述
但是还有一种类型变体叫做测试环境变体,也就是在单元测试的时候使用的变体,而测试分两种一种Android 的测试和一种Junit的本地测试。

Android 的测试默认情况只会生成Debug构建类型的变体

 1. demoNnimeiDebugAndroidTest
 2. fullNnimeiDebugAndroidTest

如果你想添加/输出/管理 测试依赖 可以用testVariants

android{
  testVariants.all { variant->
        //可在此配置变体的信息
        println "变体 ${variant.name}"
    }
}

输出:

变体 demoNnimeiDebugAndroidTest
变体 fullNnimeiDebugAndroidTest

Junit则会生成全部的构建类型变体,你可以用unitTestVariants管理/添加/输出:

unitTestVariants.all{variant->
        //可在此配置变体的信息
        println "unitTestVariants 变体 ${variant.name}"
    }

输出:

unitTestVariants 变体 demoNnimeiDebugUnitTest
unitTestVariants 变体 fullNnimeiDebugUnitTest
unitTestVariants 变体 demoNnimeiReleaseUnitTest
unitTestVariants 变体 fullNnimeiReleaseUnitTest

综上我们知道变体有三种:

  1. 开发变体
  2. Android测试变体
  3. Junit测试变体

Android Gradle变体提供的所有功能读者可自行参阅文档。我们只需要知道现在他提供了添加registerJavaGeneratingTask帮助我们完成关联类路径。

回过头来我们看下这个函数

class BaseVariant{
	void registerJavaGeneratingTask(@NonNull Task task, @NonNull File... sourceFolders);
}

BaseVariant表示某个变体如demoNnimeiDebug等。

registerJavaGeneratingTask关联一个task,编译时会自动运行这个任务,sourceFolders文件下的所有java/kotlin文件会自动加入编译路径

			//如果当前是android工程链接生成的源码路径到编译路径
                if (isAndroidProject(project)) {
                    linkAndroidProject(project);
                } else {
                    //是一个java工程
                }
                
 void linkAndroidProject(Project project) {
        if (project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class)) {
            //当前是Android 应用工程
            //Android 插件提供了扩展
            // 也就是我们经常的写法
            //
            //  android{
            //
            //  }
            //
            AppExtension extension = (AppExtension) (project.getExtensions().getByName("android"));
            extension.getApplicationVariants().all(configurationAndroidVariant(project));
            extension.getTestVariants().all(configurationAndroidVariant(project));
            extension.getUnitTestVariants().all(configurationAndroidVariant(project));

            extension.getApplicationVariants().all(new Action<ApplicationVariant>() {
                @Override
                public void execute(ApplicationVariant applicationVariant) {
                    System.out.println("Android 正式环境变体  "+applicationVariant.getName() );

                }
            });
            extension.getTestVariants().all(new Action<TestVariant>() {
                @Override
                public void execute(TestVariant testVariant) {
                    System.out.println("Android 测试环境变体 "+testVariant.getName() );
                }
            });
        } else {
            //当前是Android lib工程
            LibraryExtension extension = (LibraryExtension) (project.getExtensions().getByName("android"));
            extension.getLibraryVariants().all(configurationAndroidVariant(project));
            extension.getLibraryVariants().all(configurationAndroidVariant(project));
            extension.getUnitTestVariants().all(configurationAndroidVariant(project));

        }
    }
private Action<BaseVariant> configurationAndroidVariant(Project project) {

        return new Action<BaseVariant>() {
            @Override
            public void execute(BaseVariant libraryVariant) {
                CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
                //applicationVariant.addJavaSourceFoldersToModel();
                libraryVariant.registerJavaGeneratingTask(compilerProto, new File(compilerProto.outGeneratedDir));

            }
        };
    }

Java工程处理源码关联

相对Android工程,Java工程概念就要少很多。
主要是SourceSet的概念。
SourceSet可以简单理解为Java工程目录管理器,比如如下配置:

sourceSets {
    //自定义的一个源集合
    mySource {
        java.srcDir("src/myjava")
        resources.srcDir("my/res")
    }
    //系统默认提供
    main {

    }
}

系统默认定义了默认的SourceSet叫做main.默认的java文件目录为src/main/java,资源目录为src/main/resouce.每个SourceSet之间源码不可以相互访问
在这里插入图片描述
下面的代码就会抛出找不到符号错误

public class MainJava {
    public static void main(String[] args) {
        //不能访问另一个SourceSet代码
        MyJavaClass myJavaClass = new MyJavaClass();
    }
}

java插件在SourceSet中提供了大量的Task帮助我编译某个SourceSet代码。
在这里插入图片描述
当然在默认情况我们运行Jar命令只会打包main的java文件,而不会打包其他SourceSet的代码。
比如下面运行
./gradlew jar
得到一个jar压缩包,内容如下:
在这里插入图片描述
没有mysourceJar的任何资源。但是如果你希望打包非main资源请自定义如下任务。

tasks.create("mysourceJar", Jar) {
    from(sourceSets.mySource.output)
}

每个SourceSet都提供非常多的属性帮助我们使用,如上output表示这个SourceSet编译后输出的class文件和资源文件目录。然后我们创建一个已有的Jar任务扩展即可.关于自定任务可以参阅官网。

JavaGradle插件说明

我们现在了解了Java插件的基础知识后,回到我们的主题,如何把protoc输出目录加入编译路径上?
我们只需要将我们的编译输出proto文件的输出目录加入sourceSet即可.

 void linkJavaProject(Project project) {

        SourceSetContainer container = project.getExtensions().getByType(SourceSetContainer.class);

        //遍历所有源集合 比如main等
        for (SourceSet sourceSet : container) {
            //getCompileTaskName用于获取这个sourceSet提供的编译java文件的task
            //比如我我有一个SourceSet名为MySource,那么编译任务名称为compileMySourceJava
            //利用这个函数我们快速拿到这个Task的名称
            String compileName = sourceSet.getCompileTaskName("java");
            //获取这个Task实例
            JavaCompile javaCompile = (JavaCompile) project.getTasks().getByName(compileName);
            //执行ava编译的时候,先执行编译proto文件任务
            CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
            javaCompile.dependsOn(compilerProto);
            //proto任务的输出目录附加到sourceSet中
            sourceSet.getJava().srcDirs(compilerProto.outGeneratedDir);
        }

    }

总结

我们回顾下整个插件开发流程,我们构造了三个类:
在这里插入图片描述
CompilerProtoTask 用于负责编译整个proto文件
MyProtoConfigExtension是一个让用户可以gradle中自定配置proto文件位置的类
MyProtoPlugin负责将CompilerProtoTask输出目录加入的工程编译路径中,让其打包的时候可以寻找到。

//CompilerProtoTask.java
public class CompilerProtoTask extends DefaultTask {

    Logger logger = Logging.getLogger(CompilerProtoTask.class);

    @Input
    String protoDir;

    @OutputDirectory
    String outGeneratedDir;

    {
        outGeneratedDir = getProject().getBuildDir() + "/generated/source/protos/";
    }

    @TaskAction
    void action() {

        OsDetector osDetector = new OsDetector();


        //这个名字用于创建一个configuration,configuration可以简单理解为管理一组依赖的管理器
        String MycName = "customPluginConfiguration";
        //创建一个管理器名字为customPluginConfiguration
        Configuration configuration = getProject().getConfigurations().create(MycName);

        //这个是构造proto的artifact的信息,这些信息可以查看pom文件得知
        HashMap<String, String> protocArtifactMap = new HashMap<>();
        protocArtifactMap.put("group", "com.google.protobuf");
        protocArtifactMap.put("name", "protoc");
        protocArtifactMap.put("version", "3.14.0");
        protocArtifactMap.put("classifier", osDetector.getClassifier());
        protocArtifactMap.put("ext", "exe");

        //添加依赖到MycName这个管理器中
        Dependency protoDependency = getProject().getDependencies().add(MycName, protocArtifactMap);

        //用管理器返回这个依赖的所有的文件
        FileCollection files = configuration.fileCollection(protoDependency);
        //因为这个依赖只会存在一个文件也就是编译器
        File protoExe = files.getFiles().stream().findFirst().get();


        try {
            //获得扩展类对象实例,主要用于获取用户配置的proto文件路径
            String protoDirPath = protoDir;

            File file1 = new File(getProject().getProjectDir(), protoDirPath);
            //得到用户配置proto文件目录下的所有后缀为proto的文件
            String[] extensionFilter = {"proto"};
            Collection<File> protoDifFile = FileUtils.listFiles(new File(getProject().getProjectDir(), protoDirPath), extensionFilter, false);

            //拼接命令行字符串
            StringBuilder cmd = new StringBuilder(protoExe.getAbsolutePath() + " ");

            File outFile = new File(outGeneratedDir);
            if (!outFile.exists()) {
                outFile.mkdirs();
            }
            cmd.append("--java_out=" + outGeneratedDir);
            for (File file : protoDifFile) {
                String replaceFilePath = " " + file.getPath().replaceFirst(file1.getAbsolutePath() + "/", "") + " ";
                cmd.append(replaceFilePath);
            }
            cmd.append(" -I" + protoDirPath + " ");

            logger.info("运行编译命令 " + cmd);
            //防止编译器无权限运行
            if (!protoExe.canExecute() && !protoExe.setExecutable(true)) {
                throw new GradleException("protoc编译器无法执行");
            }


            //执行命令行
            Process exec = null;
            try {
                String[] strings = new String[0];
                exec = Runtime.getRuntime().exec(cmd.toString(), strings, getProject().getProjectDir());
                int resultCode = exec.waitFor();

                //执行成功
                if (resultCode == 0) {

                } else {
                    throw new GradleException("编译proto文件错误" + IOUtils.toString(exec.getErrorStream()));
                }
            } finally {
                if (exec != null) {
                    exec.destroy();
                }
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}
//MyProtoConfigExtension.java
public class MyProtoConfigExtension {
    //要编译的proto文件目录
    String protoDirPath;

    public String getProtoDirPath() {
        return protoDirPath;
    }

    public void setProtoDirPath(String protoDirPath) {
        this.protoDirPath = protoDirPath;
    }
}
//MyProtoPlugin.java
public class MyProtoPlugin implements Plugin<Project> {

    //gradle 日志输出工具。参数表示是否输出日志,false表示输出
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin 插件被激活");

        /**
         * 创建一个插件DSL扩展,第一个参数为DSL名字,第二个为配置类
         *  比如:
         *     protoConfig{
         *             // protoDirPath为MyProtoConfigExtension内部属性
         *             protoDirPath = "张三"
         *
         *     }
         */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);

        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {

                /**
                 * 创建任务并设置输出目录
                 */
                CompilerProtoTask compilerProto = project.getTasks().create("compilerProto", CompilerProtoTask.class);
//                compilerProto.onlyIf(new );
                compilerProto.setGroup("proto");
                MyProtoConfigExtension myProtoConfigExtension = project.getExtensions().getByType(MyProtoConfigExtension.class);
                compilerProto.protoDir = myProtoConfigExtension.protoDirPath;

                //如果当前是android工程链接生成的源码路径到编译路径
                if (isAndroidProject(project)) {
                    linkAndroidProject(project);
                } else {
                    linkJavaProject(project);
                }
            }
        });


    }

    void linkAndroidProject(Project project) {
        if (project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class)) {
            //当前是Android 应用工程
            //Android 插件提供了扩展
            // 也就是我们经常的写法
            //
            //  android{
            //
            //  }
            //
            AppExtension extension = (AppExtension) (project.getExtensions().getByName("android"));
            extension.getApplicationVariants().all(configurationAndroidVariant(project));
            extension.getTestVariants().all(configurationAndroidVariant(project));
            extension.getUnitTestVariants().all(configurationAndroidVariant(project));

            extension.getApplicationVariants().all(new Action<ApplicationVariant>() {
                @Override
                public void execute(ApplicationVariant applicationVariant) {
                    System.out.println("Android 正式环境变体  "+applicationVariant.getName() );

                }
            });
            extension.getTestVariants().all(new Action<TestVariant>() {
                @Override
                public void execute(TestVariant testVariant) {
                    System.out.println("Android 测试环境变体 "+testVariant.getName() );
                }
            });
        } else {
            //当前是Android lib工程
            LibraryExtension extension = (LibraryExtension) (project.getExtensions().getByName("android"));
            extension.getLibraryVariants().all(configurationAndroidVariant(project));
            extension.getLibraryVariants().all(configurationAndroidVariant(project));
            extension.getUnitTestVariants().all(configurationAndroidVariant(project));

        }
    }

    private Action<BaseVariant> configurationAndroidVariant(Project project) {

        return new Action<BaseVariant>() {
            @Override
            public void execute(BaseVariant libraryVariant) {
                CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
                //applicationVariant.addJavaSourceFoldersToModel();
                libraryVariant.registerJavaGeneratingTask(compilerProto, new File(compilerProto.outGeneratedDir));

            }
        };
    }


    void linkJavaProject(Project project) {

        SourceSetContainer container = project.getExtensions().getByType(SourceSetContainer.class);

        //遍历所有源集合 比如main等
        for (SourceSet sourceSet : container) {
            //getCompileTaskName用于获取这个sourceSet提供的编译java文件的task
            //比如我我有一个SourceSet名为MySource,那么编译任务名称为compileMySourceJava
            //利用这个函数我们快速拿到这个Task的名称
            String compileName = sourceSet.getCompileTaskName("java");
            //获取这个Task实例
            JavaCompile javaCompile = (JavaCompile) project.getTasks().getByName(compileName);
            //执行ava编译的时候,先执行编译proto文件任务
            CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
            javaCompile.dependsOn(compilerProto);
            //proto任务的输出目录附加到sourceSet中
            sourceSet.getJava().srcDirs(compilerProto.outGeneratedDir);
        }

    }

    //是Android工程
    boolean isAndroidProject(Project project) {
        return project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class) || project.getPlugins().hasPlugin(com.android.build.gradle.LibraryPlugin.class);
    }
}

源码地址:

本案例源码地址

参考

Android测试相关文档

Android变体说明文档

AGP源码和文档

Java插件文档

Gradle configuration相关文档

JavaGradle插件说明