前言
protobuf
是Google
推出的序列化协议,比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提供的插件帮助我们
我们给插件目录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文件
上面我们得到信息:
- 本地平台的
protoc
编译器,如果mac版本编译器 - 工程
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
无非两种:
apply plugin: 'com.android.application'
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
综上我们知道变体有三种:
- 开发变体
Android
测试变体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
任务扩展即可.关于自定任务可以参阅官网。
我们现在了解了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);
}
}
源码地址: