预期

当前安卓的一切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 即可。

结尾

终究成果大约便是原先一个Module,现在被拆分成100+的Module,而且根据buf.yaml 文件动态生成,基本符合第一期需求。

这部分计划这样也就大约完结了一半,剩余的一半咱们需求逐一把生层事务的依靠进行一次改变,这样就能够做到依靠最小化,然后也能够去除去一部分无用的代码块。