ProtoBuf动态拆分Gradle Module解析

作者:究极逮虾户 时间:2022-01-06 17:45:21 

预期

当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module

ProtoBuf动态拆分Gradle Module解析

buf.yaml

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。

在我司proto相关的都是由后端大佬们来维护的,然后这个协议仓库会被android/ios/后端/前端 依赖之后生成对应的代码,然后直接使用。

而proto文件中允许导入对于其他proto文件的依赖,所以这就导致了想要把几个proto转化成一个java-library工程,还需要考虑依赖问题。所以由 我们的后端来定义了一个buf.yaml的数据格式。

version: v1
name: buf.xxx.co/xxx/xxxxxx
deps:
 - buf.xxxxx.co/google/protobuf
build:
 excludes:
   - setting
breaking:
 use:
   - FILE
lint:
 use:
   - DEFAULT

name代表了这个工程的名字,deps则表示了他依赖的proto的工程名。基于这份yaml内容,我们就可以大概确定一个proto工程编译需要的基础条件。然后我们只需要一个工具或者插件来帮助我们生成对应的工程就够了。

模板工程

现在我们基本已经有了一个单一的proto工程的输入模型了,工程名依赖的工程还有对应文件夹下的proto文件。然后我们就可以基于这部分输入的模型,生成出第一个模板工程。

plugins {
   id 'java-library'
   id 'org.jetbrains.kotlin.jvm'
   id 'com.google.protobuf'
}

java {
   sourceCompatibility = JavaVersion.VERSION_1_8
   targetCompatibility = JavaVersion.VERSION_1_8
}

sourceSets {
   def dirs = new ArrayList<String>()
   dirs.add("src/main/proto")
   main.proto.srcDirs = dirs
}

protobuf {
   protoc {
       if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
           artifact = "com.google.protobuf:protoc:$version_protobuf_protoc:osx-x86_64"
       } else {
           artifact = "com.google.protobuf:protoc:$version_protobuf_protoc"
       }
   }
   plugins {
       grpc {
           if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
               artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1:osx-x86_64'
           } else {
               artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1'
           }
       }
   }
   generateProtoTasks {
       all().each { task ->
           task.generateDescriptorSet = true
           task.builtins {
               // In most cases you don't need the full Java output
               // if you use the lite output.
               java {

}

}
           task.plugins {
               grpc { option 'lite' }
           }
       }
   }
}
afterEvaluate {
   project.tasks.findByName("compileJava").dependsOn(tasks.findByName("generateProto"))
   project.tasks.findByName("compileKotlin").dependsOn(tasks.findByName("generateProto"))
}
dependencies {
   implementation "org.glassfish:javax.annotation:10.0-b28"
   def grpcJava = '1.36.1'
   compileOnly "io.grpc:grpc-protobuf-lite:${grpcJava}"
   compileOnly "io.grpc:grpc-stub:${grpcJava}"
   compileOnly "io.grpc:grpc-core:${grpcJava}"
   File file = new File(projectDir, "depend.txt")
   if (!file.exists()) {
       return
   }
   def lines = file.readLines()
   if (lines.isEmpty()) {
       return
   }
   lines.forEach {
       logger.lifecycle("project:" + name + "   implementation: " + it)
       implementation(it)
   }
}

如果需要将proto编译成java代码,就需要依赖于com.google.protobuf插件,依赖于上面的build.gradle基本就可以将一个proto输入编译成一个jar工程。

另外我们需要把所有的proto文件拷贝到这个壳工程的src/main/proto文件夹下,最后我们会将buf.yaml中的name: buf.xxx.co/xxx/xxxxxx/xxx/xxxxxx转化成工程名,去除掉一些无法识别的字符。

我们生成的模板工程如下:

ProtoBuf动态拆分Gradle Module解析

其中proto.version会记录proto内的gitsha值还有文件的lastModified时间,如果输入发生变更则会重新进行一次文件拷贝操作,避免重复覆盖的风险。

input.txt则包含了所有proto文件路径,方便我们进行开发调试。

deps 转化

由于proto之间存在依赖,没有依赖则会导致无法将proto转化成java。所以这里我讲buf.yaml中读取出的deps转化成了一个depend.txt.

com.xxxx.api:google-protobuf:7.7.7

depend.txt内会逐行写入当前模块的依赖,我们会对name进行一次转化,变成一个可读的gradle工程名。其中7.7.7的版本只是一个缺省而已,并没有实际的价值。

多线程操作

这里我们出现了一点点的性能问题, 如果可以gradle插件中尽量多使用点多线程,尤其是这种需要io的操作中。

这里我通过ForkJoinPool,这个是ExecutorService的实现类。其中submit方法中会返回一个ForkJoinTask,我们可以将获取gitsha值和lastModified放在这个中。之后把所有的ForkJoinTask放到一个数组中。

fun await() {
    forkJoins.forEach {
        it.join()
    }
}

然后最后暴露一个await方法,来做到所有的获取方法完成之后再继续向下执行。

另外则就是壳module的生成,我们也放在了子线程内执行。我们这次使用了线程池的invokeAll方法。

protoFileWalk.hashMap.forEach { (_, pbBufYaml) ->
          callables.add(Callable<Void> {
              val root = FileUtils.getRootProjectDir(settings.gradle)
              try {
                  val file = pbBufYaml.copyLib(File(root, "bapi"))
                  projects[pbBufYaml.projectName()] = file.absolutePath ?: ""
              } catch (e: Exception) {
                  e.printStackTrace()
                  e.message.log()
              }
              null
          })
      }
      executor.invokeAll(callables)

这里有个面试经常出现的考点,多线程操作Hashmap,之后我在测试环节随机出现了生成工程和include不匹配的问题。所以最后我更换了ConcurrentHashMap就没有出现这个问题了。

加载壳Module

这部分就和源码编译插件基本是一样的写法。

projects.forEach { (s, file) ->
             settings.include(":${s}")
             settings.project(":${s}").projectDir = File(file)
         }

把工程插入settings 即可。

结尾

这部分方案这样也就大概完成了一半,剩下的一半我们需要逐一把生层业务的依赖进行一次变更,这样就可以做到依赖最小化,然后也可以去除掉一部分无用的代码块。

来源:https://juejin.cn/post/7204279791381643322

标签:ProtoBuf,拆分,Gradle,Module
0
投稿

猜你喜欢

  • 详解C#对XML、JSON等格式的解析

    2022-06-04 22:45:50
  • 详解Spring Boot 定制HTTP消息转换器

    2023-11-24 20:20:51
  • mybatis mybatis-plus-generator+clickhouse自动生成代码案例详解

    2021-06-06 10:12:55
  • WPF使用Geometry绘制几何图形

    2023-08-14 14:13:31
  • swing分割窗口控件JSplitPane使用方法详解

    2021-07-28 14:15:20
  • 在C#使用字典存储事件示例及实现自定义事件访问器

    2022-08-14 14:34:52
  • C#写入XML文档

    2022-03-21 18:08:33
  • 解决MyEclipse10.7部署报错抛空指针异常问题的方法

    2023-10-14 23:52:34
  • 解析如何在android中增加gsensor驱动(MMA7660)

    2023-05-22 22:03:24
  • Java Set集合去重的原理及实现

    2023-08-11 10:56:11
  • UnityWebRequest前后端交互实现过程解析

    2021-09-07 01:24:42
  • SpringMVC 数据校验方法(必看篇)

    2023-11-14 21:44:05
  • WPF+SkiaSharp实现自绘弹幕效果

    2022-09-30 09:52:38
  • Android实现记住用户名和密码功能

    2023-10-06 13:02:07
  • Spring MVC+FastJson+hibernate-validator整合的完整实例教程

    2021-10-31 13:20:13
  • Spring中Bean的三种实例化方式详解

    2023-07-28 12:37:07
  • C#如何获取枚举的描述属性详解

    2023-02-23 13:49:59
  • IDEA启动tomcat控制台中文乱码问题的解决方法(100%有效)

    2021-06-25 10:45:23
  • Android 自定义标题栏 显示网页加载进度的方法实例

    2023-10-11 09:51:22
  • C++11 condition_variable条件变量的用法说明

    2021-12-06 23:39:56
  • asp之家 软件编程 m.aspxhome.com