I don't live with Groovy

After coming to the new company, PHS started to take over the core technology-quick editing plug-in. When I saw the legendary core technology, PHS was dumbfounded, ah this, the plug-in written by groovy, groovy is serious, it's 2202, what about the plug-in? I still use groovy to write it. My new handwriting plugin has also changed to kotlin. I can't write groovy in my life, don't think about it. But well, work is not shabby, just learn.

At the beginning, I chatted with a few big guys in the group, and the sharpener was ready to move the knife on the historical code, all migrated to kotlin, but found it cool. . . Hey, I don't seem to understand the code. I don't know what the corresponding writing of kt is. At the end of the article, PHS was fired.

Just kidding, I'm still on duty. Work still has to go on. Since my ability is limited, I can't move it all over, so I can write new requirements in kotlin. Hey, this is interesting.

How to mix Groovy with java and kotlin

how to mix

I won't, let's see how the official writes. The gradle source code has this code to illustrate how to prioritize groovy compilation over java compilation.

// tag::compile-task-classpath[]
tasks.named('compileGroovy') {
    // Groovy only needs the declared dependencies
    // (and not longer the output of compileJava)
    classpath = sourceSets.main.compileClasspath
}
tasks.named('compileJava') {
    // Java also depends on the result of Groovy compilation
    // (which automatically makes it depend of compileGroovy)
    classpath += files(sourceSets.main.groovy.classesDirectory)
}
// end::compile-task-classpath[]
复制代码

Oh, you can write it like this, then I can just copy it and change the name. I can write kotlin, Oye!

compileKotlin {
    classpath = sourceSets.main.compileClasspath
}
compileGroovy {
    classpath += files(sourceSets.main.kotlin.classesDirectory)
}
复制代码

Run a shot, if there is no accident, you will see this error.

image-20220404182245279.png

Hey, why can't I run after copying it? I suspect that there is a problem with the kotlin classesDirectory. The breakpoint looks at the sourceSets.main.kotlin.classesDirectory of the compileGroovy task. Probably like this, it is a DefaultDirectoryVar class.

image-20220404183057111.png

Hey, what is this, I didn't understand it at first, I think the value here is weird, and I'm not sure, then I'll see what other normal classesDirectory are

image-20220404183309092.png

In fact, it can be determined that the classDirectory of kotlin is not available at this time, confirming my own guess, try to add a breakpoint of catch, it is true

image-20220404183633448.pngSpecifically why it is not available at this time, I don't have a more detailed in-depth, if anyone knows, you can give me some advice.

SO 搜了一波解答,看到一篇靠谱的回复 compile-groovy-and-kotlin.

compileGroovy.dependsOn compileKotlin
compileGroovy.classpath += files(compileKotlin.destinationDir)
复制代码

试了一下确实是可以的,但为啥这样可以了呢?以及最上面官方的代码是啥意思呢?还有一些奇奇怪怪的名词是啥,下面吹一下

关于 souceset

我们入门写 android 时,都看到 / 写过类似这样的代码

sourceSets {
    main.java.srcDirs = ['src/java']
}
复制代码

我对他的理解是指定 main sourceset 下的 java 的源码目录。 SourceSets 是一个 Sourset 的容器用来创建一个个的 SourceSet, 比如 main, test. 而 main 下的 java, groovy, kotlin 目录是一个编译目录(SourceDirectorySet),编译实质是找到一个个的编译目录,然后将他们变成 .class 文件放在 build/classes/sourceDirectorySet 下面, 也就是 destinationDirectory。

像 main 对应的是 SourceSet 接口,其实现是 DefaultSourceSet。而 main 下面的 groovy, java, kotlin 是 SourceDirectorySet 接口,其实现是 DefaultSourceDirectorySet。

官方 gradle 对于 sourceset 的定义是:

  • the source files and where they’re located 定位源码的位置
  • the compilation classpath, including any required dependencies (via Gradle configurations) 编译时的 class path
  • where the compiled class files are placed 编译出的 class 放在哪

输入文件 + 编译时 classpath 经过 AbstractCompile Task 得到 输出的 class 目录

java sourcesets compilation

第二个 编译时的 classpath,在项目里也见过,sourceSetImplementation 声明 sourceSet 的依赖。第三个我很少见到,印象不深,SourceDirectorySet#destinationDirectory 用来指定 compile task 的输出目录。而 SourceDirectorySet#classesDirectory 和这个值是一致的。再重申一遍这里的 SourceDirectorySet 想成是 DSL 里写的 java, groovy,kt 就好了。

官方文档对于 classesDirectory 的描述是

The directory property that is bound to the task that produces the output via SourceDirectorySet.compiledBy(org.gradle.api.tasks.TaskProvider, java.util.function.Function). Use this as part of a classpath or input to another task to ensure that the output is created before it is used. Note: To define the path of the output folder use SourceDirectorySet.getDestinationDirectory()

大意是 classesDirectory 与这个 compile task 的输出是相关联的,具体是通过 SourceDirectorySet.compiledBy() 方法,这个字段由 destinationDirectory 字段决定。查看 DefaultSourceDirectorySet#compiledBy 方法

    public <T extends Task> void compiledBy(TaskProvider<T> taskProvider, Function<T, DirectoryProperty> mapping) {
        this.compileTaskProvider = taskProvider;
        taskProvider.configure(task -> {
            if (taskProvider == this.compileTaskProvider) {
                mapping.apply(task).set(destinationDirectory);
            }
        });
        classesDirectory.set(taskProvider.flatMap(mapping::apply));
    }
复制代码

雀食语义上 classesDirectory == destinationDirectory。

现在我们可以去理解下 官方的 demo 了,官方的 demo 简单说就是优先执行 Compile Groovy task, 再去执行 Compile Java task.

tasks.named('compileGroovy') {
    classpath = sourceSets.main.compileClasspath // 1
}
tasks.named('compileJava') {
    classpath += files(sourceSets.main.groovy.classesDirectory) // 2
}
复制代码

可能看不懂的地方是 1,2 注释处做了啥, 1 处我问了我们组大佬,这是重置了 compileGroovy task 的 classpath 使其不依赖 compile java classpath,在 GroovyPlugin 源码中有那么一句代码

        classpath.from((Callable<Object>) () -> sourceSet.getCompileClasspath().plus(target.files(sourceSet.getJava().getClassesDirectory())));
复制代码

可以看到 GroovyPlugin 其实是依赖于 java 的 classpath 的。这里我们需要改变 groovy 和 java 的编译时序需要把这层依赖断开。

2呢,使 compileJava 依赖上 compileGroovy 的 output property,间接使 compileJava dependson compileGroovy 任务。

具体为啥 Kotlin 的不行,俺还没搞清楚,知道的大佬可以指教下。

而 SO 上的这个答复其实也是类似的,而且更直接

compileGroovy.dependsOn compileKotlin
compileGroovy.classpath += files(compileKotlin.destinationDir)
复制代码

使 compileGroovy 依赖于 compileKotlin 任务,再让 compileGroovy 的 classPath 添加上 compileKotlin 的 output. 既然任务的 classPath 添加 另一个任务的 output 会自动依赖上另一个 task。那其实这么写也是可以的

compileGroovy.classpath += files(compileKotlin.destinationDir)
复制代码

实验了下雀食是可以跑的. 那既然 Groovy 和 Java 都包含 main 的 classpath,是不是 compileKotlin 的 classpath 置为 main,那 compileGroovy 会自动依赖上 compileKotlin。试试呗

compileKotlin.classpath = sourceSets.main.compileClasspath
复制代码

image-20220405151016867.png 可以看到 kotlin 的执行顺序雀食跑到了最前面。

在项目实操中,我发现 Kotlin 跑在了 compile 的最前面,那其实 kotlin 的类里面是不能依赖 java 或者 groovy 的任何依赖的。这也符合预期,不然就会出现依赖成环,报 Circular dependsOn hierarchy found in the Kotlin source sets 错误。我个人观点这是一种对历史代码改造的折衷,在新需求上使用 kotlin 进行开发,一些功能相同的工具类能翻译成 kt 就翻译,不能就重写一套。

小结

  • 在这节讲了两种实现混编的方案。写法不同,本质都是使一个任务依赖另一个任务的 output
// 1
compileGroovy.classpath += files(compileKotlin.destinationDir)
// 2
compileKotlin.classpath = sourceSets.main.compileClasspath
复制代码
  • 我对于 SourceSet 和 SourceDirectorySet 的理解
  • 项目中实践混编方案的现状

Groovy 有趣的语法糖

在写 Groovy 的过程中,我遇到一个头大的问题,代码看不懂,里面有一些奇奇怪怪没见过的语法糖,乍一看就懵了,你要不一起瞅瞅。

includes*.tasks

我司的仓库是大仓的结构,仓库和子仓之间是通过 Composite build 构建联系的。那么怎么使主仓的 task 触发 includeBuild 的仓库执行对应仓库呢?是通过这行代码实现的

tasks.register('publishDeps') {
    dependsOn gradle.includedBuilds*.task(':publishIvyPublicationToIvyRepository')
}
复制代码

这里的 includeBuilds*.task 后面的 *.task 是啥?includeBuilds 看源码发现是个 List。我不懂 groovy,但好歹我能看懂 kotlin, 我看看官方文档右边对应的 kt 写法是啥?

tasks.register("publishDeps") {
    dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
}
复制代码

咦嘿,原来是个 List 的 map 操作,骚里骚气的。翻了翻原来是个 groovy 的语法糖,写个代码试试看看他编译到 class 是啥样子

def list = ["1", "22", "333"]
def lengths = list*.size()
lengths.forEach{
    println it
}
复制代码

编译成 class

        Object list = ScriptBytecodeAdapter.createList(new Object[]{"1", "22", "333"});
        Object lengths = ScriptBytecodeAdapter.invokeMethod0SpreadSafe(Groovy.class, list, (String)"size");
        var1[0].call(lengths, new Groovy._closure1(this, this));
复制代码

在 ScriptBytecodeAdapter.invokeMethod0SpreadSafe 实现内部其实还是新建了一个 List 再逐个对 List 中元素进行 map.

String.execute

这是执行一个 shell 指令,比如 "ls -al".execute(), 刚看到这个的时候认为这个东西类似 kotlin 的扩展函数,点进去看实现发现不一样

public static Process execute(final String self) throws IOException {
        return Runtime.getRuntime().exec(self);
 }
复制代码

可以看到 receiver 是他的第一个参数,莫非这是通用的语法糖,我试试写了个

public static String deco(final String self) throws IOException {
        return self + "deco"
    }
// println "".deco()
复制代码

运行下,哦吼,跑不了,报了 MissingMethodException。看样子是不通用的。翻了翻 groovy 文档,找到了这个文档

Static methods are used with the first parameter being the destination class, i.e. public static String reverse(String self) provides a reverse() method for String.

看样子这个语法糖是 groovy 内部定制的,我不清楚有没有支持开发定制的方式,知道的大佬可以评论区留言下。

Range 怎么写

groovy 也有类似 kotlin 的 Range 的概念,包含的 Range 是 .. , 不包含右边界(until)的是 ..<

Try with resources

我遇到过一个 OKHttp 连接泄露的问题,代码原型大概是这样

if (xxx) {
  response.close()
} else {
  // behavior
}
复制代码

定位到是 Response 没有在 else 的分支上进行 close,当然可以简单在 else 分支上进行 close, 并在外层补上 try, catch 兜底,但在 Effective Java 一书提及针对资源关闭 try-with-resource 优于 try cactch。但我尝试像 java 一样写 try-with-resource,发现嗝屁了,直接报红,我去 SO 上搜了一波 groovy 的 try-with-resource. Groovy 是通过 withCloseable 扩展来实现,看这个方法的声明与 Process#execute 语法糖类似—public static def withCloseable(Closeable self, Closure action) . 最终改造后的代码是这样的

Response.withCloseable { reponse ->
  if (xxx) {
    
  } else {
    
  }
}
复制代码
<<

这个是 groovy 中的左移运算符也是可以重载的,而 kotlin 是不支持的。他运用比较多的场景。起初我印象中 Task 的 是覆写了这个运算符作为 doLast 简易写法,现在 gradle7.X 的版本上是没有了。其它常见的是文件写入操作, 列表添加元素。

def file = new File("xxx")
file << "text"
def list = []
list << "aaa"
复制代码

Groovy 的一家之言

如果 kotlin 是 better java, 那么 groovy 应该是 more than java,它的定位更加偏向脚本一些,更加动态化(从它反编译的字节码可见一斑),上手曲线较高,但一个人精通这个语言,并且独立维护一个项目,其实 groovy 的开发效率并不会比 kotlin 和 java 差,感受比较深切的是 maven publish 的例子,看看插件中 groovy 和 kotlin 的写法上的不同。

// Groovy
def mavenSettings = {
            groupId 'org.gradle.sample'
            artifactId 'library'
            version '1.1'
        }
 def repSettings = {
            repositories {
                maven {
                    url = mavenUrl
                }
            }
        }
​
afterEvaluate {
  publishing {
      publications {
          maven(MavenPublication) {
              ConfigureUtil.configure(mavenSettings, it)
              from components.java
          }
      }
     ConfigureUtil.configure(repoSettings, it)
  }
  def publication = publishing.publications.'maven' as MavenPublication
  publication.pom.withXml { 
     // inject msg
  }
}
复制代码
// Kotlin
// Codes are borrowed from (sonatype-publish-plugin)[https://github.com/johnsonlee/sonatype-publish-plugin/]
fun Project.publishing(
        config: PublishingExtension.() -> Unit
) = extensions.configure(PublishingExtension::class.java, config)
val Project.publishing: PublishingExtension
    get() = extensions.getByType(PublishingExtension::class.java)
​
val mavenClosure = closureOf<MavenPublication> { 
   groupId = "org.gradle.sample"
   artifactId = "library"
   version = "1.1"
}
val repClosure = closureOf<PublishingExtension> {
    repositories {
        maven {
            url = mavenUrl
        }
    }
}
afterEvaluate {
    publishing {
        publications {
            create<MavenPublication>("maven") {
               ConfigureUtil.configure(mavenClosure, this)
                from(components["java"])
            }
        }
       ConfigureUtil.configure(repoClosure, this)
    }
  
    val publication = publishing.publications["maven"] as MavenPublication
    publication.pom.withXml { 
             // inject msg
    }
}
复制代码

我觉得吧,如果像我们大佬擅长 groovy 的话,而且是一个人开发的商业项目,插件里的确写 groovy 会更快,更简洁,那为什么不呢?这对他来说是种善,语言没有优劣,动态性和静态语言优劣我不想较高下,这因人而异。 image.png 我选择 kotlin 是俺不擅长写 groovy 啊,我写了几个月 groovy 每次改动插件发布后再应用第一次都会有语法错误,调试的头皮发麻,所以最后搞了个折衷方案,新代码用 kotlin, 旧代码用 groovy 继续写。而且参考了 KOGE@2BAB 文档,发现咦,gradle 正面回应过 groovy 与 kotlin 之争. "Prefer using a statically-typed language to implement a plugin"@Gradle。嗯, 我还是继续写 Kotlin 吧。

Guess you like

Origin juejin.im/post/7084949825866694686