preface

Protobuf is a serialization protocol developed by Google. Protobuf takes up fewer bytes than JSON and serializes faster. This article briefly introduces protocol specificities and then shows you how to write a Gradle plug-in for automatic compilation.

Protobuf Github address Protobuf syntax tutorial

An intuitive example:

  val protoBufPerson = AddressBookProtos
            .Person
            .newBuilder()
            .setEmail("[email protected]")
            .setId(23)
            .setName("Fifty").build()
        val jsonPerson = "" "{" email ":" [email protected] ", "id" : 23, "name" : "detective"} "" "

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

Output:

 protoBufPerson :22  jsonPerson :46
Copy the code

A protobuf is about half the size of a JSON data body when serialized. You can refer to the official website for specific performance.

Tip: Mobile should consider using the Lite version to reduce the generated class size

Protobuf Usage flow

As the official website has a detailed introduction, here is only a brief description.

1 Edit the proto file

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;

}
Copy the code

Proto syntax is cross-language, so we need the compiler tools of the corresponding platform to compile into Java files. For example, the author needs to download the compiler tools of this platform under MacOs.

2 Run the compilation tool to compile the intermediate syntax file into a Java file:

Protoc -- javA_out = Output directory compile file -i = folder where compile file residesCopy the code

3 Copy and generate Java files to the project

In order to improve the development efficiency, we have already launched gradle plug-in to help us complete automatic compilation and automatically add to the project directory.

The following willprotoFile into the directory can be directly usedPerson.protoCompiled product

Start writing embedded plug-ins

Build. Gradle: Build. Gradle: build. Gradle: build. Gradle: build. Gradle: build. Gradle: build. Gradle: build. Developing Custom Gradle Plugins.

This example is shown in an Android project directory:

├── ├─ ├─ Class exercises, class Exercises, class Exercises, class Exercises, class Exercises, class Exercises, class Exercises, class Exercises, class Exercises BuildSrc │ └ ─ ─ build. Gradle ├ ─ ─ gradle │ └ ─ ─ wrapper ├ ─ ─ gradle. Properties ├ ─ ─ gradlew ├ ─ ─ gradlew. Bat ├ ─ ─ Local. The properties └ ─ ─ Settings. GradleCopy the code

Base class writing

Start by editing a plug-in class and implementing the Plugin interface

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

    // Gradle log output tool. The parameter indicates whether to output logs. False indicates whether to output logs
    Logger logger = new Logger(false);

	// Callback when the plug-in is applied
    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin is activated"); }}Copy the code

Then apply it under build.gradle in app module

//build.gradle
apply plugin:MyProtoPlugin
Copy the code

Run. / gradlew: app: help

Output:

/gradlew -q :app:help MyProtoPlugin is activatedCopy the code

Obtain the proto file directory of user configuration

Our plugin requires the user to tell the location of the proto file that needs to be compiled, so we expose a DSL for developers to customize.

public class MyProtoPlugin implements Plugin<Project> {

    // Gradle log output tool. The parameter indicates whether to output logs. False indicates whether to output logs
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin is activated");

        /** * Create a plug-in DSL extension with the first parameter as the DSL name and the second as the configuration class * for example: * protoConfig{*/ protoDirPath is an internal attribute of MyProtoConfigExtension * protoDirPath = "Zhang 3" * *} */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);


        // Wait until the project configuration phase is complete to output the user configuration
        project.afterEvaluate(project1 -> {
            MyProtoConfigExtension configExtension = project1.getExtensions().getByType(MyProtoConfigExtension.class);
            logger.log("User configured directory"+ configExtension.protoDirPath); }); }}public class MyProtoConfigExtension {
	// Directory of the proto file to compile
    String protoDirPath;

    public String getProtoDirPath(a) {
        return protoDirPath;
    }

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

The plug-in application modifies the code

//build.gradle
apply plugin: MyProtoPlugin
// Configure the location of the protobuf file to compile
protoConfig {
    protoDirPath = "src/main/proto"
}
Copy the code

Get the proto compiler corresponding to the user’s current system

Some developers useMacOr,linuxAccording to this, we need to select a corresponding version of the compiler to compileprotoFile.

GoogleReleased aartifact: com.google.protobuf:protoc

We look at the Artifact catalog:



We can see that there are many versions of the compiler in this artifact. But suppose we just want to takemacWhat about the compiler? We can look at thatprotoc-xxx.pomfile

We can see that poM provides class classifiers (classifierFor us to choose.classifierMaven is a basic knowledge of Maven. If you don’t know, you can look at the wheels on the webartifactwithThe organization name.Name of the artifact.The version number.Optional classifier.The optional file suffix extEtc.)



Protoc File directory

For example, if we need a compiler for MAC, we can write it as follows

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

Now that we know how to pull the compiler for the platform from the artifact, how do we determine the operating system for the current Gradle execution environment? We can use plugins provided by Google to help us

osdetector-gradle-plugin

We add dependencies to build.gradle in the plugin directory buildSrc

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

Continue editing the plug-in


public class MyProtoPlugin implements Plugin<Project> {

    // Gradle log output tool. The parameter indicates whether to output logs. False indicates whether to output logs
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
         / /...
        // Use osDetector to get the corresponding system classifier
        OsDetector osDetector = new OsDetector();
        logger.log("Current operating system classifiers" +osDetector.getClassifier());
		/ /...}}Copy the code

The output

The classifier for the current operating system is OSX-X86_64Copy the code

We finally get the Protoc compiler Maven


		OsDetector osDetector = new OsDetector();
        logger.log("Current operating system classifiers" + osDetector.getClassifier());


        // This name is used to create a configuration, which simply means to manage a set of dependent managers
        // Implementation and testImplementation are one of the managers
        String MycName = "customPluginConfiguration";
        / / create a manager name for customPluginConfiguration
        Configuration configuration = project.getConfigurations().create(MycName);

        // This is information about building the Artifact for proto, which can be found in the POM file
        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");

        // Add dependencies to the MycName manager
        Dependency protoDependency = project.getDependencies().add(MycName, protocArtifactMap);

        // Use the manager to return all the files for this dependency
        FileCollection files = configuration.fileCollection(protoDependency);
        // Because the dependency will only exist in one file, the compiler
        File protoExe = files.getSingleFile();
        logger.log("Obtained platform compiler" + protoExe.getAbsolutePath());
Copy the code

Perform compilation of protoc files

Above we get the message:

  1. Local platformprotocCompiler, if the MAC version compiler
  2. engineeringprotoThe file path

We write a Task that compiles and outputs results.

//CompilerProtoTask.java
public class CompilerProtoTask extends DefaultTask {

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

    @Input
    String protoDir;

	// Output the compiled folder
    @OutputDirectory
    String outGeneratedDir;

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

    @TaskAction
    void action(a) {

        OsDetector osDetector = new OsDetector();


        // This name is used to create a configuration, which simply means to manage a set of dependent managers
        String MycName = "customPluginConfiguration";
        / / create a manager name for customPluginConfiguration
        Configuration configuration = getProject().getConfigurations().create(MycName);

        // This is information about building the Artifact for proto, which can be found in the POM file
        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");

        // Add dependencies to the MycName manager
        Dependency protoDependency = getProject().getDependencies().add(MycName, protocArtifactMap);

        // Use the manager to return all the files for this dependency
        FileCollection files = configuration.fileCollection(protoDependency);
        // Because the dependency will only exist in one file, the compiler
        File protoExe = files.getFiles().stream().findFirst().get();


        try {
            // Get the extension object instance, mainly used to get the proto file path configured by the user
            String protoDirPath = protoDir;

            File file1 = new File(getProject().getProjectDir(), protoDirPath);
            // Get all proto files in the proto file directory of the user configuration
            String[] extensionFilter = {"proto"};
            Collection<File> protoDifFile = FileUtils.listFiles(new File(getProject().getProjectDir(), protoDirPath), extensionFilter, false);

            // Concatenates command line strings
            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("Run the compile command" + cmd);
            // Prevent the compiler from running without permission
            if(! protoExe.canExecute() && ! protoExe.setExecutable(true)) {
                throw new GradleException("Protoc compiler cannot execute.");
            }


            // Execute the command line
            Process exec = null;
            try {
                String[] strings = new String[0];
                exec = Runtime.getRuntime().exec(cmd.toString(), strings, getProject().getProjectDir());
                int resultCode = exec.waitFor();

                // The command is successfully executed
                if (resultCode == 0) {}else {
                    throw new GradleException("Error compiling proto file"+ IOUtils.toString(exec.getErrorStream())); }}finally {
                if(exec ! =null) { exec.destroy(); }}}catch(Exception e) { e.printStackTrace(); }}}Copy the code

The Task above simply gets the compiler and then executes shell commands to compile the source file.

We integrated the above Task into the plug-in

public class MyProtoPlugin implements Plugin<Project> {

    // Gradle log output tool. The parameter indicates whether to output logs. False indicates whether to output logs
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin is activated");

        /** * Create a plug-in DSL extension with the first parameter as the DSL name and the second as the configuration class * for example: * protoConfig{*/ protoDirPath is an internal attribute of MyProtoConfigExtension * protoDirPath = "Zhang 3" * *} */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);

		// Add task to Gradle after EVALUATE
        project.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(Project project) {

                /** * Create the task and set the output directory */
                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; }}); }}Copy the code

Execute the following commands will be in the build/granerated/source/protos generate Java files

./gradlew compilerProto
Copy the code



Although the plug-in generates the source file, the Java compiler does not know that the file is packaged into the project. Like generatingA.javaWe want Gradle to automatically compile this file for me and finally package itjarorapkIn the.

The associated plug-in generates the class into the compile path

We generated the class files above, but they are not yet associated with the compile path, meaning that our generated classes are not packaged into jars or Android apK.

We need to know if the current project is Java or Android.

The Android project handles source code association

For Android projects, AGP will be referenced in two ways:

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

The former is an Apk application and the latter is an Android class library.

We add Android plugin dependencies to build.gradle under plugin buildSrc:

dependencies {
	/ /...
    implementation  'com. Android. Tools. Build: gradle: 4.2.0 - alpha16'
	/ /..
}
Copy the code

Therefore, the logic to determine whether the project of the application plug-in is Android is as follows:

 // This is an Android project
    boolean isAndroidProject(Project project) {
        return project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class) || project.getPlugins().hasPlugin(com.android.build.gradle.LibraryPlugin.class);
    }
Copy the code

Library is the LibraryPlugin class used by apply Plugin: ‘com.android.application’

Why can a string be associated with a plugin class

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

The plug-in configuration will be released in time to generate a data located in the SRC/main/resources/meta-inf/gradle – plugins/org. Samples. The greeting. The content of the properties are as follows

implementation-class=org.gradle.GreetingPlugin
Copy the code

We know how to distinguish between Android projects and Java projects (either Android projects or Java projects), but we haven’t told the Android plugin to add our generated proto directory to the compile path, but AGP provides a corresponding function for us to do so.

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

In case you are not familiar with the Android Gradle plugin, here are some explanations for building variations and flavors:

The following cases:

android{
 // Build type
 buildTypes {
        release {
           
        }
        debug {

        }

    }
    // Flavor dimensions
    flavorDimensions "version"."hha"
	// Product flavor
    productFlavors {
        demo {
            dimension "version"
            applicationIdSuffix ".demo"
            versionNameSuffix "-demo"
        }
        full {
            dimension "version"
            applicationIdSuffix ".full"
            versionNameSuffix "-full"
        }
        nnimei {
            dimension "hha"
            applicationIdSuffix ".full"
            versionNameSuffix "-full"}}}Copy the code

It will combine the different dimensions of the product flavor and finally combine them into the build type.

In the case above: two dimensions version and HHA combined product flavor, there are two results respectively:

1. fullnnimei
2. demonnimei
Copy the code

Combining build types

1.fullnnimeiDebug
2.demonnimeiDebug
3.fullnnimeiRelease
4.demonnimeiRelease
Copy the code

This results in four final variants.



But there is another type of variation called test environment variation, which is the variation used in unit testing, and there are two types of testing: Android testing and Junit native testing.

Android tests only generate variations of the Debug build type by default

 1. demoNnimeiDebugAndroidTest
 2. fullNnimeiDebugAndroidTest
Copy the code

If you want to add/export/manage test dependencies you can use testVariants:

android{
  testVariants.all { variant->
        // You can configure variant information here
        println "Variant ${variant. The name}"}}Copy the code

Output:

Variant demoNnimeiDebugAndroidTest variants fullNnimeiDebugAndroidTestCopy the code

Junit will generate all build type variants which you can manage/add/export using unitTestVariants:

UnitTestVariants. All {variants -> println "unitTestVariants ${variants. Name}"}Copy the code

Output:

A later version of the unittestiphone variants will follow FullNnimeiReleaseUnitTest demoNnimeiReleaseUnitTest unitTestVariants optionCopy the code

To sum up, we know that there are three variants:

  1. Development variations
  2. AndroidTest variations
  3. JunitTest variations

All features provided by Android Gradle variants are documented for your own use. We just need to know now he provides added registerJavaGeneratingTask help us complete the association class path.

So let’s go back and look at this function

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

BaseVariant represents a variant such as demoNnimeiDebug.

RegisterJavaGeneratingTask associated with a task, the compiler automatically run the task, sourceFolders all Java/kotlin of file automatically add compiler path

			// If the current is android project link generated source path to compile path
                if (isAndroidProject(project)) {
                    linkAndroidProject(project);
                } else {
                    // Is a Java project
                }
                
 void linkAndroidProject(Project project) {
        if (project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class)) {
            // Currently Android application project
            //Android plugins provide extensions
            //
            //
            // 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 Official Environment Variants"+applicationVariant.getName() ); }}); extension.getTestVariants().all(new Action<TestVariant>() {
                @Override
                public void execute(TestVariant testVariant) {
                    System.out.println("Android Test Environment Variants"+testVariant.getName() ); }}); }else {
            // This is an Android lib project
            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, newFile(compilerProto.outGeneratedDir)); }}; }Copy the code

Java engineering handles source code association

Java engineering is much less conceptual than Android engineering. Mainly the concept of SourceSet. SourceSet can be understood simply as the Java project directory manager, as shown in the following configuration:

sourceSets {
    // A custom collection of sources
    mySource {
        java.srcDir("src/myjava")
        resources.srcDir("my/res")}// Provided by default
    main {

    }
}
Copy the code

The system defines the defaultSourceSetcalledmain. The defaultjavaThe file directory issrc/main/java, the resource directory issrc/main/resouceEach.SourceSetThe source code can not be accessed from each other



The following code throws a symbol not found error

public class MainJava {
    public static void main(String[] args) {
        // Cannot access another SourceSet code
        MyJavaClass myJavaClass = newMyJavaClass(); }}Copy the code

Java plug-in inSourceSetProvides a large number ofTaskHelp me compile somethingSourceSetThe code.



Of course, by default, running the Jar command will only package the Java files in main, not anything elseSourceSetThe code.

So let’s say I run this

./gradlew jar

Get a jar package with the following contents:



There is nomysourceJarAny resource. However, if you want to package non-main resources, please customize the following tasks.

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

Each SourceSet provides a number of attributes to help us use, such as output representing the class file and resource file directory that the SourceSet compiled. Then we can create an existing Jar task extension. See the official website for custom tasks.

JavaGradle plug-in description

Now that we know the basics of Java plug-ins, back to our topic, how to add the Protoc output directory to the build path? All we need to do is add the output directory of our compiled output proto file to sourceSet.

 void linkJavaProject(Project project) {

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

        // Iterate over all source collections such as main etc
        for (SourceSet sourceSet : container) {
            //getCompileTaskName is used to get the Task for compiling Java files provided by sourceSet
            // If I have a SourceSet named MySource, the task name will be compileMySourceJava
            // Use this function to quickly get the name of the Task
            String compileName = sourceSet.getCompileTaskName("java");
            // Get the Task instance
            JavaCompile javaCompile = (JavaCompile) project.getTasks().getByName(compileName);
            // Compile the proto file first
            CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
            javaCompile.dependsOn(compilerProto);
            // The output directory of the proto task is attached to sourceSetsourceSet.getJava().srcDirs(compilerProto.outGeneratedDir); }}Copy the code

conclusion

To review the entire plug-in development process, we constructed three classes:



CompilerProtoTaskResponsible for compiling the entire Proto file

MyProtoConfigExtensionGradle is a class that allows users to customize the location of proto files

MyProtoPluginResponsible for theCompilerProtoTaskThe output directory is added to the project compilation path so that it can be found when packaging.

//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(a) {

        OsDetector osDetector = new OsDetector();


        // This name is used to create a configuration, which simply means to manage a set of dependent managers
        String MycName = "customPluginConfiguration";
        / / create a manager name for customPluginConfiguration
        Configuration configuration = getProject().getConfigurations().create(MycName);

        // This is information about building the Artifact for proto, which can be found in the POM file
        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");

        // Add dependencies to the MycName manager
        Dependency protoDependency = getProject().getDependencies().add(MycName, protocArtifactMap);

        // Use the manager to return all the files for this dependency
        FileCollection files = configuration.fileCollection(protoDependency);
        // Because the dependency will only exist in one file, the compiler
        File protoExe = files.getFiles().stream().findFirst().get();


        try {
            // Get the extension object instance, mainly used to get the proto file path configured by the user
            String protoDirPath = protoDir;

            File file1 = new File(getProject().getProjectDir(), protoDirPath);
            // Get all proto files in the proto file directory of the user configuration
            String[] extensionFilter = {"proto"};
            Collection<File> protoDifFile = FileUtils.listFiles(new File(getProject().getProjectDir(), protoDirPath), extensionFilter, false);

            // Concatenates command line strings
            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("Run the compile command" + cmd);
            // Prevent the compiler from running without permission
            if(! protoExe.canExecute() && ! protoExe.setExecutable(true)) {
                throw new GradleException("Protoc compiler cannot execute.");
            }


            // Execute the command line
            Process exec = null;
            try {
                String[] strings = new String[0];
                exec = Runtime.getRuntime().exec(cmd.toString(), strings, getProject().getProjectDir());
                int resultCode = exec.waitFor();

                // The command is successfully executed
                if (resultCode == 0) {}else {
                    throw new GradleException("Error compiling proto file"+ IOUtils.toString(exec.getErrorStream())); }}finally {
                if(exec ! =null) { exec.destroy(); }}}catch(Exception e) { e.printStackTrace(); }}}Copy the code
//MyProtoConfigExtension.java
public class MyProtoConfigExtension {
    // Directory of the proto file to compile
    String protoDirPath;

    public String getProtoDirPath(a) {
        return protoDirPath;
    }

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

    // Gradle log output tool. The parameter indicates whether to output logs. False indicates whether to output logs
    Logger logger = new Logger(false);

    @Override
    public void apply(Project project) {
        logger.log("MyProtoPlugin is activated");

        /** * Create a plug-in DSL extension with the first parameter as the DSL name and the second as the configuration class * for example: * protoConfig{*/ protoDirPath is an internal attribute of MyProtoConfigExtension * protoDirPath = "Zhang 3" * *} */
        project.getExtensions()
                .create("protoConfig", MyProtoConfigExtension.class);

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

                /** * Create the task and set the output directory */
                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;

                // If the current is android project link generated source path to compile path
                if (isAndroidProject(project)) {
                    linkAndroidProject(project);
                } else{ linkJavaProject(project); }}}); }void linkAndroidProject(Project project) {
        if (project.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class)) {
            // Currently Android application project
            //Android plugins provide extensions
            //
            //
            // 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 Official Environment Variants"+applicationVariant.getName() ); }}); extension.getTestVariants().all(new Action<TestVariant>() {
                @Override
                public void execute(TestVariant testVariant) {
                    System.out.println("Android Test Environment Variants"+testVariant.getName() ); }}); }else {
            // This is an Android lib project
            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, newFile(compilerProto.outGeneratedDir)); }}; }void linkJavaProject(Project project) {

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

        // Iterate over all source collections such as main etc
        for (SourceSet sourceSet : container) {
            //getCompileTaskName is used to get the Task for compiling Java files provided by sourceSet
            // If I have a SourceSet named MySource, the task name will be compileMySourceJava
            // Use this function to quickly get the name of the Task
            String compileName = sourceSet.getCompileTaskName("java");
            // Get the Task instance
            JavaCompile javaCompile = (JavaCompile) project.getTasks().getByName(compileName);
            // Compile the proto file first
            CompilerProtoTask compilerProto = (CompilerProtoTask) project.getTasks().getByName("compilerProto");
            javaCompile.dependsOn(compilerProto);
            // The output directory of the proto task is attached to sourceSetsourceSet.getJava().srcDirs(compilerProto.outGeneratedDir); }}// This is an Android project
    boolean isAndroidProject(Project project) {
        returnproject.getPlugins().hasPlugin(com.android.build.gradle.AppPlugin.class) || project.getPlugins().hasPlugin(com.android.build.gradle.LibraryPlugin.class); }}Copy the code

Source code address:

This case source address

reference

Documentation related to Android testing

Android variant documentation

AGP source code and documentation

Java Plug-in Documentation

Gradle Configuration documents

JavaGradle plug-in description