标签 Groovy 下的文章

awk 和 Groovy 相辅相成,可以创建强大、有用的脚本。

最近我写了一个使用 Groovy 脚本来清理我的音乐文件中的标签的系列。我开发了一个 框架,可以识别我的音乐目录的结构,并使用它来遍历音乐文件。在该系列的最后一篇文章中,我从框架中分离出一个实用类,我的脚本可以用它来处理文件。

这个独立的框架让我想起了很多 awk 的工作方式。对于那些不熟悉 awk 的人来说,你学习下这本电子书:

《awk 实用指南》

我从 1984 年开始大量使用 awk,当时我们的小公司买了第一台“真正的”计算机,它运行的是 System V Unix。对我来说,awk 是非常完美的:它有 关联内存 associative memory ——将数组视为由字符串而不是数字来索引的。它内置了正则表达式,似乎专为处理数据而生,尤其是在处理数据列时,而且结构紧凑,易于学习。最后,它非常适合在 Unix 工作流使用,从标准输入或文件中读取数据并写入到输出,数据不需要经过其他的转换就出现在了输入流中。

说 awk 是我日常计算工具箱中的一个重要部分一点也不为过。然而,在我使用 awk 的过程中,有几件事让我感到不满意。

可能主要的问题是 awk 善于处理以分隔字段呈现的数据,但很奇怪它不善于处理 CSV 文件,因为 CSV 文件的字段被引号包围时可以嵌入逗号分隔符。另外,自 awk 发明以来,正则表达式已经有了很大的发展,我们需要记住两套正则表达式的语法规则,而这并不利于编写无 bug 的代码。一套这样的规则已经很糟糕了

由于 awk 是一门简洁的语言,因此它缺少很多我认为有用的东西,比如更丰富的基础类型、结构体、switch 语句等等。

相比之下,Groovy 拥有这些能力:可以使用 OpenCSV 库,它很擅长处理 CSV 文件、Java 正则表达式和强大的匹配运算符、丰富的基础类型、类、switch 语句等等。

Groovy 所缺乏的是简单的面向管道的概念,即把要处理数据作为一个传入的流,以及把处理过的数据作为一个传出的流。

但我的音乐目录处理框架让我想到,也许我可以创建一个 Groovy 版本的 awk “引擎”。这就是我写这篇文章的目的。

安装 Java 和 Groovy

Groovy 是基于 Java 的,需要先安装 Java。最新的、合适的 Java 和 Groovy 版本可能都在你的 Linux 发行版的软件库中。Groovy 也可以按照 Groovy 主页 上的说明进行安装。对于 Linux 用户来说,一个不错的选择是 SDKMan,它可以用来获得多个版本的 Java、Groovy 和其他许多相关工具。在这篇文章中,我使用的是 SDK 的版本:

  • Java:OpenJDK 11 的 11.0.12 的开源版本
  • Groovy:3.0.8

使用 Groovy 创建 awk

这里的基本想法是将打开一个或多个文件进行处理、将每行分割成字段、以及提供对数据流的访问等复杂情况封装在三个部分:

  • 在处理数据之前
  • 在处理每行数据时
  • 在处理完所有数据之后

我并不打算用 Groovy 来取代 awk。相反,我只是在努力实现我的典型用例,那就是:

  • 使用一个脚本文件而不是在命令行写代码
  • 处理一个或多个输入文件
  • 设置默认的分隔符为 |,并基于这个分隔符分割所有行
  • 使用 OpenCSV 完成分割工作(awk 做不到)

框架类

下面是用 Groovy 类实现的 “awk 引擎”:

@Grab('com.opencsv:opencsv:5.6')
import com.opencsv.CSVReader
public class AwkEngine {
    // With admiration and respect for
    //     Alfred Aho
    //     Peter Weinberger
    //     Brian Kernighan
    // Thank you for the enormous value
    // brought my job by the awk
    // programming language
    Closure onBegin
    Closure onEachLine
    Closure onEnd
    private String fieldSeparator
    private boolean isFirstLineHeader
    private ArrayList<String> fileNameList
    public AwkEngine(args) {
        this.fileNameList = args
        this.fieldSeparator = "|"
        this.isFirstLineHeader = false
    }
    public AwkEngine(args, fieldSeparator) {
        this.fileNameList = args
        this.fieldSeparator = fieldSeparator
        this.isFirstLineHeader = false
    }
    public AwkEngine(args, fieldSeparator, isFirstLineHeader) {
        this.fileNameList = args
        this.fieldSeparator = fieldSeparator
        this.isFirstLineHeader = isFirstLineHeader
    }
    public void go() {
        this.onBegin()
        int recordNumber = 0
        fileNameList.each { fileName ->
            int fileRecordNumber = 0
            new File(fileName).withReader { reader ->
                def csvReader = new CSVReader(reader,
                    this.fieldSeparator.charAt(0))
                if (isFirstLineHeader) {
                    def csvFieldNames = csvReader.readNext() as
                        ArrayList<String>
                    csvReader.each { fieldsByNumber ->
                        def fieldsByName = csvFieldNames.
                            withIndex().
                            collectEntries { name, index ->
                                [name, fieldsByNumber[index]]
                            }
                        this.onEachLine(fieldsByName,
                                recordNumber, fileName,
                                fileRecordNumber)
                        recordNumber++
                        fileRecordNumber++
                    }
                } else {
                    csvReader.each { fieldsByNumber ->
                        this.onEachLine(fieldsByNumber,
                            recordNumber, fileName,
                            fileRecordNumber)
                        recordNumber++
                        fileRecordNumber++
                    }
                }
            }
        }
        this.onEnd()
    }
}

虽然这看起来是相当多的代码,但许多行是因为太长换行了(例如,通常你会合并第 38 行和第 39 行,第 41 行和第 42 行,等等)。让我们逐行看一下。

第 1 行使用 @Grab 注解从 Maven Central 获取 OpenCSV 库的 5.6 本周。不需要 XML。

第 2 行我引入了 OpenCSV 的 CSVReader

第 3 行,像 Java 一样,我声明了一个 public 实用类 AwkEngine

第 11-13 行定义了脚本所使用的 Groovy 闭包实例,作为该类的钩子。像任何 Groovy 类一样,它们“默认是 public”,但 Groovy 将这些字段创建为 private,并对其进行外部引用(使用 Groovy 提供的 getter 和 setter 方法)。我将在下面的示例脚本中进一步解释这个问题。

第 14-16 行声明了 private 字段 —— 字段分隔符,一个指示文件第一行是否为标题的标志,以及一个文件名的列表。

第 17-31 行定义了三个构造函数。第一个接收命令行参数。第二个接收字段的分隔符。第三个接收指示第一行是否为标题的标志。

第 31-67 行定义了引擎本身,即 go() 方法。

第 33 行调用了 onBegin() 闭包(等同于 awk 的 BEGIN {} 语句)。

第 34 行初始化流的 recordNumber(等同于 awk 的 NR 变量)为 0(注意我这里是从 00 而不是 1 开始的)。

第 35-65 行使用 each {} 来循环处理列表中的文件。

第 36 行初始化文件的 fileRecordNumber(等同于 awk 的 FNR 变量)为 0(从 0 而不是 1 开始)。

第 37-64 行获取一个文件对应的 Reader 实例并处理它。

第 38-39 行获取一个 CSVReader 实例。

第 40 行检测第一行是否为标题。

如果第一行是标题,那么在 41-42 行会从第一行获取字段的标题名字列表。

第 43-54 行处理其他的行。

第 44-48 行把字段的值复制到 name:value 的映射中。

第 49-51 行调用 onEachLine() 闭包(等同于 awk 程序 BEGIN {}END {} 之间的部分,不同的是,这里不能输入执行条件),传入的参数是 name:value 映射、处理过的总行数、文件名和该文件处理过的行数。

第 52-53 行是处理过的总行数和该文件处理过的行数的自增。

如果第一行不是标题:

第 56-62 行处理每一行。

第 57-59 调用 onEachLine() 闭包,传入的参数是字段值的数组、处理过的总行数、文件名和该文件处理过的行数。

第 60-61 行是处理过的总行数和该文件处理过的行数的自增。

第 66 行调用 onEnd() 闭包(等同于 awk 的 END {})。

这就是该框架的内容。现在你可以编译它:

$ groovyc AwkEngine.groovy

一点注释:

如果传入的参数不是一个文件,编译就会失败,并出现标准的 Groovy 堆栈跟踪,看起来像这样:

Caught: java.io.FileNotFoundException: not-a-file (No such file or directory)
java.io.FileNotFoundException: not-a-file (No such file or directory)
at AwkEngine$_go_closure1.doCall(AwkEngine.groovy:46)

OpenCSV 可能会返回 String[] 值,不像 Groovy 中的 List 值那样方便(例如,数组没有 each {})。第 41-42 行将标题字段值数组转换为 list,因此第 57 行的 fieldsByNumber 可能也应该转换为 list。

在脚本中使用这个框架

下面是一个使用 AwkEngine 来处理 /etc/group 之类由冒号分隔并没有标题的文件的简单脚本:

def ae = new AwkEngine(args, ':')
int lineCount = 0
ae.onBegin = {
    println “in begin”
}
ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
    if (lineCount < 10)
        println “fileName $fileName fields $fields”
    lineCount++
}
ae.onEnd = {
    println “in end”
    println “$lineCount line(s) read”
}
ae.go()

第 1 行 调用的有两个参数的构造函数,传入了参数列表,并定义冒号为分隔符。

第 2 行定义一个脚本级的变量 lineCount,用来记录处理过的行数(注意,Groovy 闭包不要求定义在外部的变量为 final)。

第 3-5 行定义 onBegin() 闭包,在标准输出中打印出 “in begin” 字符串。

第 6-10 行定义 onEachLine() 闭包,打印文件名和前 10 行字段,无论是否为前 10 行,处理过的总行数 lineCount 都会自增。

第 11-14 行定义 onEnd() 闭包,打印 “in end” 字符串和处理过的总行数。

第 15 行运行脚本,使用 AwkEngine

像下面一样运行一下脚本:

$ groovy Test1Awk.groovy /etc/group
in begin
fileName /etc/group fields [root, x, 0, ]
fileName /etc/group fields [daemon, x, 1, ]
fileName /etc/group fields [bin, x, 2, ]
fileName /etc/group fields [sys, x, 3, ]
fileName /etc/group fields [adm, x, 4, syslog,clh]
fileName /etc/group fields [tty, x, 5, ]
fileName /etc/group fields [disk, x, 6, ]
fileName /etc/group fields [lp, x, 7, ]
fileName /etc/group fields [mail, x, 8, ]
fileName /etc/group fields [news, x, 9, ]
in end
78 line(s) read
$

当然,编译框架类生成的 .class 文件需要在 classpath 中,这样才能正常运行。通常你可以用 jar 把这些 class 文件打包起来。

我非常喜欢 Groovy 对行为委托的支持,这在其他语言中需要各种诡异的手段。许多年来,Java 需要匿名类和相当多的额外代码。Lambda 已经在很大程度上解决了这个问题,但它们仍然不能引用其范围之外的非 final 变量。

下面是另一个更有趣的脚本,它很容易让人想起我对 awk 的典型使用方式:

def ae = new AwkEngine(args, ';', true)
ae.onBegin = {
    // nothing to do here
}
def regionCount = [:]
    ae.onEachLine = { fields, recordNumber, fileName, fileRecordNumber ->
        regionCount[fields.REGION] =
            (regionCount.containsKey(fields.REGION) ?
                regionCount[fields.REGION] : 0) +
            (fields.PERSONAS as Integer)
}
ae.onEnd = {
    regionCount.each { region, population ->
        println “Region $region population $population”
    }
}
ae.go()

第 1 行调用了三个函数的构造方法,true 表示这是“真正的 CSV” 文件,第一行为标题。由于它是西班牙语的文件,因此它的逗号表示数字的,标准的分隔符是分号。

第 2-4 行定义 onBegin() 闭包,这里什么也不做。

第 5 行定义一个(空的)LinkedHashmap,键是 String 类型,值是 Integer 类型。数据文件来自于智利最近的人口普查,你要在这个脚本中计算出智利每个地区的人口数量。

第 6-11 行处理文件中的行(加上标题一共有 180,500 行)—— 请注意在这个案例中,由于你定义 第 1 行为 CSV 列的标题,因此 fields 参数会成为 LinkedHashMap<String,String> 实例。

第 7-10 行是 regionCount 映射计数增加,键是 REGION 字段的值,值是 PERSONAS 字段的值 —— 请注意,与 awk 不同,在 Groovy 中你不能在赋值操作的右边使用一个不存在的映射而期望得到空值或零值。

第 12-16 行,打印每个地区的人口数量。

第 17 行运行脚本,调用 AwkEngine

像下面一样运行一下脚本:

$ groovy Test2Awk.groovy ~/Downloads/Censo2017/ManzanaEntidad_CSV/Censo*csv
Region 1 population 330558
Region 2 population 607534
Region 3 population 286168
Region 4 population 757586
Region 5 population 1815902
Region 6 population 914555
Region 7 population 1044950
Region 8 population 1556805
Region 16 population 480609
Region 9 population 957224
Region 10 population 828708
Region 11 population 103158
Region 12 population 166533
Region 13 population 7112808
Region 14 population 384837
Region 15 population 226068
$

以上为全部内容。对于那些喜欢 awk 但又希望得到更多的东西的人,我希望你能喜欢这种 Groovy 的方法。


via: https://opensource.com/article/22/9/awk-groovy

作者:Chris Hermansen 选题:lkxed 译者:lxbwolf 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

为了简化 Java 的繁琐,我制作了一个 Groovy 工具来分析我的音乐目录。

最近,我一直在研究 Groovy 是如何简化略微繁琐的 Java 的。在这篇文章中,我开始了一个简短的系列,通过创建一个分析我的音乐目录的工具来演示 Groovy 脚本。

在本文中,我将演示 groovy.File 类如何扩展和精简 java.File 并简化其使用。这为查看音乐文件夹的内容提供了一个框架,以确保预期的内容(例如,cover.jpg 文件)就位。我使用 JAudiotagger 库 来分析音乐文件的标签。

安装 Java 和 Groovy

Groovy 基于 Java,需要安装 Java。 Java 和 Groovy 的最新和稳定的版本可能都在你的 Linux 发行版的仓库中。 Groovy 也可以直接从 Apache Foundation 网站 安装。对于 Linux 用户来说,一个不错的选择是 SDKMan,它可用于获取 Java、Groovy 和许多其他相关工具的多个版本。对于本文,我使用以下 SDK 版本:

  • Java:版本 11.0.12-open 的 OpenJDK 11
  • Groovy:版本 3.0.8

音乐元数据

最近,我重整了我的音乐消费方式。我决定使用优秀的开源 Cantata 音乐播放器,它是开源 MPD 音乐播放器 的一个前端。我所有的电脑的音乐都存储在 /var/lib/mpd/music 目录下。在该音乐目录下有艺术家子目录,在每个艺术家子目录下有专辑子目录,包含音乐文件、cover.jpg,偶尔还有 PDF 格式的内页说明。

我绝大部分的音乐文件都是 FLAC 格式的,有一些是 MP3 格式,可能还有一小部分是 OGG 格式。我选择 JAudiotagger 库的一个原因是它可以透明地处理不同的标签格式。当然,JAudiotagger 是开源的!

那么查看音频标签有什么意义呢?以我的经验,音频标签的管理极差。(提到音频标签,)我的脑海中浮现出“粗心”这个词。这是标签本身真正存在的问题,也可能是出于我自己的学究倾向。无论如何,这是一个可以通过使用 Groovy 和 JAudiotagger 解决的重要问题。不过,它不仅适用于音乐收藏。许多其他现实世界的问题也适用,如需要下沉到文件系统中的目录树来处理在那里找到的内容。

使用 Groovy 脚本

这是此任务所需的基本代码。我在脚本中加入了注释,这些注释反映了我通常留给自己的(相对简写的)“注释提醒”:

// 定义音乐库目录
def musicLibraryDirName = '/var/lib/mpd/music'
// 输出 CSV 文件标题行
println "artistDir|albumDir|contentFile"
// 迭代音乐库目录中的每个目录
// 这一层应该是艺术家目录
new File(musicLibraryDirName).eachDir { artistDir ->
    // 迭代艺术家目录中的每个目录
    // 这一层应该是专辑目录
    artistDir.eachDir { albumDir ->
        // 迭代专辑目录中的每个目录
        // 这里应该是内容
        // 或相关内容(如 `cover.jpg`,PDF 格式的内页说明)
        albumDir.eachFile { contentFile ->
            println "$artistDir.name|$albumDir.name|$contentFile.name"
        }
    }
}

如上所述,我使用 groovy.File 在目录树中移动。具体来说:

第 7 行创建一个新的 groovy.File 对象并在其上调用 groovy.File.eachDir(),第 7 行的 { 和第 18 行的结尾的 } 之间的代码是传给 eachDir()groovy.Colsue 参数。

这意味着 eachDir() 为目录中找到的每个子目录执行该代码。这类似于 Java lambda(也称为“匿名函数”)。 Groovy 闭包不会像 lambda 那样限制对调用环境的访问(在最新版本的 Groovy 中,如果你愿意,也可以使用 Java lambda)。如上所述,音乐库目录中的子目录应该是艺术家目录(例如,“Iron Butterfly” 或 “Giacomo Puccini”),因此 artistDireachDir() 传递给闭包的参数。

第 10 行对每个 artistDir 调用 eachDir(),第 10 行的 { 和第 17 行的 } 之间的代码形成另一个处理 albumDir 的闭包。

第 14 行,在每个 albumDir 上调用 eachFile(),第 14 行的 { 和第 16 行的 } 之间的代码形成了处理专辑内容的第三级闭包。

在本文的范围内,我对每个文件唯一需要做的就是开始构建信息表,我将其创建为一个以竖线分隔的 CSV 文件,它可以导入 LibreOfficeOfficeOnly 或任何其他电子表格。现在,代码输出前三列:艺术家目录名、专辑目录名和内容文件名(同样,第 2 行输出 CSV 标题行)。

在我的 Linux 笔记本电脑上运行它会产生以下输出:

$ groovy TagAnalyzer.groovy | head
artistDir|albumDir|contentFile
Habib Koite & Bamada|Afriki|02 - Ntesse.flac
Habib Koite & Bamada|Afriki|08 - NTeri.flac
Habib Koite & Bamada|Afriki|01 - Namania.flac
Habib Koite & Bamada|Afriki|07 - Barra.flac
Habib Koite & Bamada|Afriki|playlist.m3u
Habib Koite & Bamada|Afriki|04 - Fimani.flac
Habib Koite & Bamada|Afriki|10 - Massake.flac
Habib Koite & Bamada|Afriki|11 - Titati.flac
Habib Koite & Bamada|Afriki|03 – Africa.flac
[...]
Richard Crandell|Spring Steel|04-Japanese Lullaby [Richard Crandell].flac
Richard Crandell|Spring Steel|Spring Steel.pdf
Richard Crandell|Spring Steel|03-Zen Dagger [Richard Crandell].flac
Richard Crandell|Spring Steel|cover.jpg
$

在性能方面:

$ time groovy TagAnalyzer.groovy | wc -l
9870

real        0m1.482s
user        0m4.392s
sys        0m0.230s
$

又好又快。它在一秒半内处理近 10,000 个文件!对我来说足够快。可观的性能、紧凑且可读的代码,还有什么不喜欢的?

在我的下一篇文章中,我会打开 JAudiotagger 并查看每个文件中的标签。


via: https://opensource.com/article/22/8/groovy-script-java-music

作者:Chris Hermansen 选题:lkxed 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

首先在 Java 中创建初始化一个整数列表,然后在 Groovy 中做同样的事。

 title=

我非常喜欢 Groovy 编程语言。我喜欢它是因为我喜欢 Java,尽管 Java 有时候感觉很笨拙。正因为我是那么喜欢 Java,其他运行在 JVM 上语言都不能吸引我。比方说 Kotlin、Scala 还有 Clojure 语言,它们感觉上就和 Java 不一样,因为它们对于什么是好的编程语言的理解不同。Groovy 和它们都不一样,在我看来,Groovy 是一个完美的选项,特别是对于一部分程序员来说,他们喜欢 Java,但是又需要一个更灵活、更紧凑,并且有时候更直接的语言。

列表 List 这种数据结构是一个很好的例子,它可以容纳一个无序的列表,列表中的元素可以是数字、字符串或者对象,程序员可以用某种方式高效地遍历这些元素,特别是对于编写和维护脚本的人来说,“高效”的关键就是要有简洁清晰的表达,而不需要一大堆“仪式”,把代码的意图都变模糊了。

安装 Java 和 Groovy

Groovy 是基于 Java 的,因此需要同时安装一个 Java 才行。你的 Linux 发行版的仓库中可能有最近的比较好的 Java 版本。或者,你也可以在根据 这些指导 来安装 Groovy。对于 Linux 用户来说,SDKMan 是一个不错的代替选项,你可以使用它来获取多个 Java 和 Groovy 版本,以及许多其他的相关工具。在这篇文章中,我使用的 SDK 发行版是:

  • Java: OpenJDK 11 的 11.0.12-open 版本
  • Groovy: 3.0.8 版本

言归正传

Java 中有很多方法可以实例化并初始化列表,从它最初被引入的时候就有了(我记得是在 Java 1.5 的时候,但请不要引用我的话)。在这些方法里,有两个有趣的方法,它们涉及到了 java.util.Arraysjava.util.List 这两个类。

使用 java.util.Arrays 类

java.util.Arrays 类定义了一个 asList() 静态方法,它可以被用来创建一个基于数组的列表,因此大小是不可变的,尽管其中的元素是可以被修改的。下面是它的使用方式:

var a1 = Arrays.asList(1,2,3,4,5,6,7,8,9,10); // immutable list of mutable elements

System.out.println("a1 = " + a1);
System.out.println("a1 is an instance of " + a1.getClass());

// output is
// a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// a1 is an instance of class java.util.Arrays$ArrayList

a1.set(0,0); // succeeds
System.out.println("a1 = " + a1); // output is
// a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10]

a1.add(11); // fails producing
// Exception in thread "main" java.lang.UnsupportedOperationException
System.out.println("a1 = " + a1); // not reached

使用 java.util.List 类

java.util.List 类定义了一个 of() 静态方法,它可以被用来创建一个不可变的列表,其中的元素是否可变要取决于它们本身是否支持修改。下面是它的使用方式:

var a2 = List.of(1,2,3,4,5,6,7,8,9,10);

System.out.println("a2 = " + a2);
System.out.println("a2 is an instance of " + a2.getClass());

// output is
// a2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// a2 is an instance of class java.util.ImmutableCollections$ListN

a2.set(0,0); // fails producing
// Exception in thread "main" java.lang.UnsupportedOperationException
System.out.println("a2 = " + a2); // not reached

a2.add(11); // also fails for same reason if above two lines commented out
System.out.println("a2 = " + a2); // not reached

因此,我可以使用 Arrays.asList(),也可以使用 List.of() 方法,前提是如果我想要的是一个大小不能改变、且不关心元素是否可变的列表。

如果我想要初始化一个可变的列表,我更倾向于把这些不可变的列表作为参数传给一个列表构造器,就像下面这样:

var a1 = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));

System.out.println("a1 = " + a1);
System.out.println("a1 is an instance of " + a1.getClass());

// output is
// a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// a1 is an instance of class java.util.ArrayList

a1.set(0,0);
System.out.println("a1 = " + a1);

//output is
// a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10]

a1.add(11);
System.out.println("a1 = " + a1);

// output is
// a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

注意,这个 Arrays.asList() 方法是用来初始化这个新的 ArrayList<Integer>() 的,也就是说,它为这个传进来的列表创建了一个可变的拷贝。

现在,或许只有我这么想,但是这种方式确实看起来需要理解很多关于 java.util.Arraysjava.util.List 类的细节才行,而我只是想要创建并初始化一个数字列表而已(尽管真正使用到的语句并没有太多“仪式”)。下面是真正用到的那行代码,仅供参考:

var a1 = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));

Groovy 是怎么做的

下面来看看在 Groovy 中如何实现上述需求:

def a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

println "a1 = $a1"
println "a1 is an instance of ${a1.getClass()}"

// output is
// a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// a1 is an instance of class java.util.ArrayList

a1[0] = 0
println "a1 = $a1"

// output is
// a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10]

a1 << 11
println "a1 = $a1"

// output is
// a1 = [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

我们一眼就能发现,Groovy 使用了 def 关键字而不是 var 关键字。我还发现了,仅仅是把一系列的类型(在这个例子里是整数)放进括号里,我就得到了一个创建好的列表。此外,这样创建出来的列表完全就是我想要的:一个可变的 ArrayList 实例。

现在,或许再一次只有我这么想,但是上面的代码看起来要简单多得多 —— 不用记住 .of().asList() 返回的是“ 半不变 semi-mutable ”的结果,也不用为它们做一些补偿。另外一个好处是,我现在可以使用括号和下标来引用列表中的某个特定元素,而不用这个叫 set() 方法。另外,这个跟在列表后面的 << 操作符也很方便,我再也不用调用 add() 方法来添加元素啦。还有,你注意到代码中没有分号了吗?没错,在 Groovy 里,句末的分号并不是必须的。最后,我们来看看字符串插值,只要在字符串里用 $变量 或者 ${表达式} 就可以实现了哦!

在 Groovy 世界中还藏着许多“有待发掘”的东西。上面的列表定义其实是一个动态类型(Groovy 中默认)和 Java 中的静态类型的对比。在上面的 Groovy 代码定义的那一行,变量 a1 的类型是在运行的时候,根据等号右边的表达式的计算结果推断出来的。现在我们都知道,动态语言可以给我们带来强大的功能,有了强大的功能,我们有了很多机会去尝试不同的东西。对于那些不喜欢动态类型的程序员来说,Groovy 也支持静态类型。

Groovy 相关资源

Apache Groovy 网站上有非常多的文档。另一个很棒的 Groovy 资源是 Mr. Haki。学习 Groovy 还有一个很棒的原因,那就是可以接着学习 Grails,后者是一个优秀的、高效率的全栈 Web 框架,基于许多优秀组件构建而成,比如有 Hibernate、Spring Boot 和 Micronaut 等。


via: https://opensource.com/article/22/1/creating-lists-groovy-java

作者:Chris Hermansen 选题:lujun9972 译者:lkxed 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Java 和 Groovy 中的 映射 map 都是非常通用的,它允许 关键字 key value 为任意类型,只要继承了 Object 类即可。

 title=

我最近在探索 Java 与 Groovy 在 创建并初始化 列表 List 在运行时构建 列表 List 方面的一些差异。我观察到,就实现这些功能而言,Groovy 的简洁和 Java 的繁复形成了鲜明对比。

在这篇文章中,我将实现在 Java 和 Groovy 中创建并初始化 映射 Map 。映射为开发支持根据 关键字 key 检索的结构提供了可能,如果找到了这样一个关键字,它就会返回对应的 value 。今天,很多编程语言都实现了映射,其中包括 Java 和 Groovy,也包括了 Python(它将映射称为 字典 dict )、Perl、awk 以及许多其他语言。另一个经常被用来描述映射的术语是 关联数组 associative array ,你可以在 这篇维基百科文章 中了解更多。Java 和 Groovy 中的映射都是非常通用的,它允许关键字和值为任意类型,只要继承了 Object 类即可。

安装 Java 和 Groovy

Groovy 基于 Java,因此你需要先安装 Java。你的 Linux 发行版的仓库中可能有最近的比较好的 Java 和 Groovy 版本。或者,你也可以在根据上面链接中的指示来安装 Groovy。对于 Linux 用户来说,SDKMan 是一个不错的代替选项,你可以使用它来获取多个 Java 和 Groovy 版本,以及许多其他的相关工具。在这篇文章中,我使用的 SDK 发行版是:

  • Java: version 11.0.12-open of OpenJDK 11;
  • Groovy: version 3.0.8.

言归正传

Java 提供了非常多的方式来实例化和初始化映射,并且从 Java 9 之后,添加了一些新的方式。其中最明显的方式就是使用 java.util.Map.of() 这个静态方法,下面介绍如何使用它:

var m1 = Map.of(
    "AF", "Afghanistan",
    "AX", "Åland Islands",
    "AL", "Albania",
    "DZ", "Algeria",
    "AS", "American Samoa",
    "AD", "Andorra",
    "AO", "Angola",
    "AI", "Anguilla",
    "AQ", "Antarctica");

System.out.println("m1 = " + m1);
System.out.println("m1 is an instance of " + m1.getClass());

事实证明,在此种情况下,Map.of() 有两个重要的限制。其一,这样创建出来的映射实例是 不可变的 immutable 。其二,你最多只能提供 20 个参数,用来表示 10 个 键值对 key-value pair

你可以尝试着添加第 10 对和第 11 对,比方说 "AG", "Antigua and Barbuda" 和 "AR", "Argentina",然后观察会发生什么。你将发现 Java 编译器尝试寻找一个支持 11 个键值对的 Map.of() 方法而遭遇失败。

快速查看 java.util.Map 类的文档,你就会找到上述第二个限制的原因,以及解决这个难题的一种方式:

var m2 = Map.ofEntries(
    Map.entry("AF", "Afghanistan"),
    Map.entry("AX", "Åland Islands"),
    Map.entry("AL", "Albania"),
    Map.entry("DZ", "Algeria"),
    Map.entry("AS", "American Samoa"),
    Map.entry("AD", "Andorra"),
    Map.entry("AO", "Angola"),
    Map.entry("AI", "Anguilla"),
    Map.entry("AQ", "Antarctica"),
    Map.entry("AG", "Antigua and Barbuda"),
    Map.entry("AR", "Argentina"),
    Map.entry("AM", "Armenia"),
    Map.entry("AW", "Aruba"),
    Map.entry("AU", "Australia"),
    Map.entry("AT", "Austria"),
    Map.entry("AZ", "Azerbaijan"),
    Map.entry("BS", "Bahamas"),
    Map.entry("BH", "Bahrain"),
    Map.entry("BD", "Bangladesh"),
    Map.entry("BB", "Barbados")
);
       
System.out.println("m2 = " + m2);
System.out.println("m2 is an instance of " + m2.getClass());

这就是一个比较好的解决方式,前提是我不在随后的代码里改变使用 Map.ofEntries() 创建并初始化的映射内容。注意,我在上面使用了 Map.ofEntries() 来代替 Map.of()

然而,假设我想要创建并初始化一个非空的映射,随后往这个映射中添加数据,我需要这样做:

var m3 = new HashMap<String,String>(Map.ofEntries(
    Map.entry("AF", "Afghanistan"),
    Map.entry("AX", "Åland Islands"),
    Map.entry("AL", "Albania"),
    Map.entry("DZ", "Algeria"),
    Map.entry("AS", "American Samoa"),
    Map.entry("AD", "Andorra"),
    Map.entry("AO", "Angola"),
    Map.entry("AI", "Anguilla"),
    Map.entry("AQ", "Antarctica"),
    Map.entry("AG", "Antigua and Barbuda"),
    Map.entry("AR", "Argentina"),
    Map.entry("AM", "Armenia"),
    Map.entry("AW", "Aruba"),
    Map.entry("AU", "Australia"),
    Map.entry("AT", "Austria"),
    Map.entry("AZ", "Azerbaijan"),
    Map.entry("BS", "Bahamas"),
    Map.entry("BH", "Bahrain"),
    Map.entry("BD", "Bangladesh"),
    Map.entry("BB", "Barbados")
));

System.out.println("m3 = " + m3);
System.out.println("m3 is an instance of " + m3.getClass());

m3.put("BY", "Belarus");
System.out.println("BY: " + m3.get("BY"));

这里,我把使用 Map.ofEntries() 创建出来的不可变映射作为 HashMap 的一个构造参数,以此创建了该映射的一个 可变副本 mutable copy ,之后我就可以修改它 —— 比如使用 put() 方法。

让我们来看看上述过程如何用 Groovy 来实现:

def m1 = [
    "AF": "Afghanistan",
    "AX": "Åland Islands",
    "AL": "Albania",
    "DZ": "Algeria",
    "AS": "American Samoa",
    "AD": "Andorra",
    "AO": "Angola",
    "AI": "Anguilla",
    "AQ": "Antarctica",
    "AG": "Antigua and Barbuda",
    "AR": "Argentina",
    "AM": "Armenia",
    "AW": "Aruba",
    "AU": "Australia",
    "AT": "Austria",
    "AZ": "Azerbaijan",
    "BS": "Bahamas",
    "BH": "Bahrain",
    "BD": "Bangladesh",
    "BB": "Barbados"]

println "m1 = $m1"
println "m1 is an instance of ${m1.getClass()}"

m1["BY"] = "Belarus"
println "m1 = $m1"

只看一眼,你就会发现 Groovy 使用了 def 关键字而不是 var —— 尽管在 最近模型 late-model 的 Groovy(version 3+)中,使用 var 关键字也是可行的。

你还会发现,你是通过在括号里添加了一个键值对列表来创建一个映射的。不仅如此,这样创建的列表对象还非常有用,这里有几个原因。其一,它是可变的;其二,它是一个 LinkedHashMap 的实例,内部维持了数据的插入顺序。所以,当你运行 Java 版本的代码并打印出变量 m3,你会看到:

m3 = {BB=Barbados, BD=Bangladesh, AD=Andorra, AF=Afghanistan, AG=Antigua and Barbuda, BH=Bahrain, AI=Anguilla, AL=Albania, AM=Armenia, AO=Angola, AQ=Antarctica, BS=Bahamas, AR=Argentina, AS=American Samoa, AT=Austria, AU=Australia, DZ=Algeria, AW=Aruba, AX=Åland Islands, AZ=Azerbaijan}

而当你运行 Groovy 版本的代码,你会看到:

m1 = [AF:Afghanistan, AX:Åland Islands, AL:Albania, DZ:Algeria, AS:American Samoa, AD:Andorra, AO:Angola, AI:Anguilla, AQ:Antarctica, AG:Antigua and Barbuda, AR:Argentina, AM:Armenia, AW:Aruba, AU:Australia, AT:Austria, AZ:Azerbaijan, BS:Bahamas, BH:Bahrain, BD:Bangladesh, BB:Barbados]

再一次,你将看到 Groovy 是如何简化事情的。这样的语法非常直观,有点像 Python 里的字典,并且,即使你有一个超过 10 个键值对的初始列表,你也不需要去记住各种必要的别扭方式。注意我们使用的表达式:

m1[“BY”] = “Belarus”

而在 Java 中,你需要这样做:

m1.put(“BY”, “Belarus”)

还有,这个映射默认是可变的,这么做的利弊很难评判,还是得取决于你的需求是什么。我个人觉得,Java 在这种情况下的 “默认不可变” 机制,最让我困扰的地方是,它没有一个类似于 Map.mutableOfMutableEntries() 的方法。这迫使一些刚学会如何声明和初始化一个映射的程序员,不得不转念去思考该如何把他们手中不可变的映射,转换为可变的。同时我也想问,创建一个不可变的对象然后再舍弃它,这样真的好吗?

另一个值得考虑的事情是,Groovy 使用方括号代替 Java 中的 put()get() 方法来进行关键字查找。因此你可以这样写:

m1[“ZZ”] = m1[“BY”]

而不需要这样写:

m1.put(“ZZ”,m1.get(“BY”))

有时候,就像使用某个类的实例变量一样来使用映射中的关键字和值是一个好办法。设想你现在有一堆想要设置的属性,在 Groovy 中,它们看起来就像下面这样:

def properties = [
      verbose: true,
      debug: false,
      logging: false]

然后,你可以改变其中的某个属性,就像下面这样:

properties.verbose = false

之所以这样能工作,是因为,只要关键字符合特定的规则,你就可以省略引号,然后直接用点操作符来代替方括号。尽管这个功能非常有用,也非常好用,它也同时也意味着,如果你要把一个变量作为一个映射的关键字来使用,你就必须把这个变量包裹在圆括号里,就像下面这样:

def myMap = [(k1): v1, (k2): v2]

是时候告诉勤奋的读者 Groovy 是一门为编写脚本而量身定制的语言了。映射通常是脚本中的关键元素,它为脚本提供了 查找表 lookup table ,并且通常起到了作为内存数据库的作用。我在这里使用的例子是 ISO 3166 规定的两个字母的国家代码和国家名称。对在世界上各个国家的互联网使用者来说,这些代码是很熟悉的。此外,假设我们要编写一个从日志文件中查找互联网主机名,并借此来了解用户的地理位置分布的脚本工具,那么这些代码会是十分有用的部分。

Groovy 相关资源

Apache Groovy 网站 上有非常多的文档。另一个很棒的 Groovy 资源是 Mr. HakiBaeldung 网站 提供了大量 Java 和 Groovy 的有用教程。学习 Groovy 还有一个很棒的原因,那就是可以接着学习 Grails,后者是一个优秀的、高效率的全栈 Web 框架。它基于许多优秀组件构建而成,比如有 Hibernate、Spring Boot 和 Micronaut 等。


via: https://opensource.com/article/22/3/maps-groovy-vs-java

作者:Chris Hermansen 选题:lujun9972 译者:lkxed 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

抛开关于是否使用 JSON 作为配置格式的争论,只需学习如何用 Groovy 来解析它。

 title=

应用程序通常包括某种类型的默认或“开箱即用”的状态或配置,以及某种让用户根据自己的需要定制配置的方式。

例如,LibreOffice Writer 通过其菜单栏上的工具 > 选项,可以访问诸如用户数据、字体、语言设置等(以及更多的)设置。一些应用程序(如 LibreOffice)提供了一个点选式的用户界面来管理这些设置。有些,像 Tracker(GNOME 的“任务”,用于索引文件)使用 XML 文件。还有一些,特别是基于 JavaScript 的应用,使用 JSON,尽管它有许多人抗议(例如,这位作者这位其他作者)。

在这篇文章中,我将回避关于是否使用 JSON 作为配置文件格式的争论,并解释如何使用 Groovy 编程语言 来解析这类信息。Groovy 以 Java 为基础,但有一套不同的设计重点,使 Groovy 感觉更像 Python。

安装 Groovy

由于 Groovy 是基于 Java 的,它也需要安装 Java。你可能会在你的 Linux 发行版的软件库中找到最近的、合适的 Java 和 Groovy 版本。或者,你可以按照其网站上的 说明 安装 Groovy。 Linux 用户的一个不错的选择是 SDKMan,你可以使用它来获取 Java、Groovy 和许多其他相关工具的多个版本。 对于本文,我将使用我的发行版的 OpenJDK11 和 SDKMan 的 Groovy 3.0.7。

演示的 JSON 配置文件

在这个演示中,我从 Drupal 中截取了这个 JSON 文件,它是 Drupal CMS 使用的主要配置文件,并将其保存在文件 config.json 中:

{
 "vm": {
  "ip": "192.168.44.44",
  "memory": "1024",
  "synced_folders": [
   {
    "host_path": "data/",
    "guest_path": "/var/www",
    "type": "default"
   }
  ],
  "forwarded_ports": []
 },
 "vdd": {
  "sites": {
   "drupal8": {
    "account_name": "root",
    "account_pass": "root",
    "account_mail": "[email protected]",
    "site_name": "Drupal 8",
    "site_mail": "[email protected]",
    "vhost": {
     "document_root": "drupal8",
     "url": "drupal8.dev",
     "alias": ["www.drupal8.dev"]
    }
   },
   "drupal7": {
    "account_name": "root",
    "account_pass": "root",
    "account_mail": "[email protected]",
    "site_name": "Drupal 7",
    "site_mail": "[email protected]",
    "vhost": {
     "document_root": "drupal7",
     "url": "drupal7.dev",
     "alias": ["www.drupal7.dev"]
    }
   }
  }
 }
}

这是一个漂亮的、复杂的 JSON 文件,有几层结构,如:

<>.vdd.sites.drupal8.account_name

和一些列表,如:

<>.vm.synced_folders

这里,<> 代表未命名的顶层。让我们看看 Groovy 是如何处理的。

用 Groovy 解析 JSON

Groovy 自带的 groovy.json 包,里面有各种很酷的东西。其中最好的部分是 JsonSlurper 类,它包括几个 parse() 方法,可以将 JSON 转换为 Groovy 的 Map,一种根据键值存储的数据结构。

下面是一个简短的 Groovy 程序,名为 config1.groovy,它创建了一个 JsonSlurper 实例,然后调用其中的 parse() 方法来解析文件中的 JSON,并将其转换名为 configMap 实例,最后将该 map 输出:

import groovy.json.JsonSlurper

def jsonSlurper = new JsonSlurper()

def config = jsonSlurper.parse(new File('config.json'))

println "config = $config"

在终端的命令行上运行这个程序:

$ groovy config1.groovy
config = [vm:[ip:192.168.44.44, memory:1024, synced_folders:[[host_path:data/, guest_path:/var/www, type:default]], forwarded_ports:[]], vdd:[sites:[drupal8:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 8, site_mail:[email protected], vhost:[document_root:drupal8, url:drupal8.dev, alias:[www.drupal8.dev]]], drupal7:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 7, site_mail:[email protected], vhost:[document_root:drupal7, url:drupal7.dev, alias:[www.drupal7.dev]]]]]]
$

输出显示了一个有两个键的顶层映射:vmvdd。每个键都引用了它自己的值的映射。注意 forwarded_ports 键所引用的空列表。

这很容易,但它所做的只是把东西打印出来。你是如何获得各种组件的呢?下面是另一个程序,显示如何访问存储在 config.vm.ip 的值:

import groovy.json.JsonSlurper

def jsonSlurper = new JsonSlurper()

def config = jsonSlurper.parse(new File('config.json'))

println "config.vm.ip = ${config.vm.ip}"

运行它:

$ groovy config2.groovy
config.vm.ip = 192.168.44.44
$

是的,这也很容易。 这利用了 Groovy 速记,这意味着:

config.vm.ip

在 Groovy 中等同于:

config['vm']['ip']

configconfig.vm 都是 Map 的实例,并且都等同于在 Java 中的:

config.get("vm").get("ip")

仅仅是处理 JSON 就这么多了。如果你想有一个标准的配置并让用户覆盖它呢?在这种情况下,你可能想在程序中硬编码一个 JSON 配置,然后读取用户配置并覆盖任何标准配置的设置。

假设上面的配置是标准的,而用户只想覆盖其中的一点,只想覆盖 vm 结构中的 ipmemory 值,并把它放在 userConfig.json 文件中:

{
 "vm": {
  "ip": "201.201.201.201",
  "memory": "4096",
 }
}

你可以用这个程序来做:

import groovy.json.JsonSlurper

def jsonSlurper = new JsonSlurper()

// 使用 parseText() 来解析一个字符串,而不是从文件中读取。
// 这给了我们一个“标准配置”
def standardConfig = jsonSlurper.parseText("""
{
 "vm": {
  "ip": "192.168.44.44",
  "memory": "1024",
  "synced_folders": [
   {
    "host_path": "data/",
    "guest_path": "/var/www",
    "type": "default"
   }
  ],
  "forwarded_ports": []
 },
 "vdd": {
  "sites": {
   "drupal8": {
    "account_name": "root",
    "account_pass": "root",
    "account_mail": "[email protected]",
    "site_name": "Drupal 8",
    "site_mail": "[email protected]",
    "vhost": {
     "document_root": "drupal8",
     "url": "drupal8.dev",
     "alias": ["www.drupal8.dev"]
    }
   },
   "drupal7": {
    "account_name": "root",
    "account_pass": "root",
    "account_mail": "[email protected]",
    "site_name": "Drupal 7",
    "site_mail": "[email protected]",
    "vhost": {
     "document_root": "drupal7",
     "url": "drupal7.dev",
     "alias": ["www.drupal7.dev"]
    }
   }
  }
 }
}
""")

// 打印标准配置
println "standardConfig = $standardConfig"

//读入并解析用户配置信息
def userConfig = jsonSlurper.parse(new File('userConfig.json'))

// 打印出用户配置信息
println "userConfig = $userConfig"

// 一个将用户配置与标准配置合并的函数
def mergeMaps(Map input, Map merge) {
  merge.each { k, v -&gt;
    if (v instanceof Map)
      mergeMaps(input[k], v)
    else
      input[k] = v
  }
}

// 合并配置并打印出修改后的标准配置
mergeMaps(standardConfig, userConfig)

println "modified standardConfig $standardConfig"

以下列方式运行:

$ groovy config3.groovy
standardConfig = [vm:[ip:192.168.44.44, memory:1024, synced_folders:[[host_path:data/, guest_path:/var/www, type:default]], forwarded_ports:[]], vdd:[sites:[drupal8:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 8, site_mail:[email protected], vhost:[document_root:drupal8, url:drupal8.dev, alias:[www.drupal8.dev]]], drupal7:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 7, site_mail:[email protected], vhost:[document_root:drupal7, url:drupal7.dev, alias:[www.drupal7.dev]]]]]]
userConfig = [vm:[ip:201.201.201.201, memory:4096]]
modified standardConfig [vm:[ip:201.201.201.201, memory:4096, synced_folders:[[host_path:data/, guest_path:/var/www, type:default]], forwarded_ports:[]], vdd:[sites:[drupal8:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 8, site_mail:[email protected], vhost:[document_root:drupal8, url:drupal8.dev, alias:[www.drupal8.dev]]], drupal7:[account_name:root, account_pass:root, account_mail:[email protected], site_name:Drupal 7, site_mail:[email protected], vhost:[document_root:drupal7, url:drupal7.dev, alias:[www.drupal7.dev]]]]]]
$

modified standardConfig 开头的一行显示,vm.ip and vm.memory 的值被覆盖了。

眼尖的读者会注意到,我没有检查畸形的 JSON,也没有仔细确保用户的配置是有意义的(不创建新字段,提供合理的值,等等)。所以用这个递归方法来合并两个映射在现实中可能并不那么实用。

好吧,我必须为家庭作业留下 一些 东西,不是吗?

Groovy 资源

Apache Groovy 网站有很多很棒的 文档。另一个很棒的 Groovy 资源是 Mr. Haki。学习 Groovy 的一个非常好的理由是继续学习 Grails,它是一个非常高效的全栈 Web 框架,建立在 Hibernate、Spring Boot 和 Micronaut 等优秀组件之上。


via: https://opensource.com/article/21/6/groovy-parse-json

作者:Chris Hermansen 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文介绍如何构建一个基于 Grails 的数据浏览器来可视化复杂的表格数据。

我是 Grails 的忠实粉丝。当然,我主要是热衷于利用命令行工具来探索和分析数据的数据从业人员。数据从业人员经常需要查看数据,这也意味着他们通常拥有优秀的数据浏览器。利用 Grails、jQuery,以及 DataTables jQuery 插件,我们可以制作出非常友好的表格数据浏览器。

DataTables 网站提供了许多“食谱式”的教程文档,展示了如何组合一些优秀的示例应用程序,这些程序包含了完成一些非常漂亮的东西所必要的 JavaScript、HTML,以及偶尔出现的 PHP。但对于那些宁愿使用 Grails 作为后端的人来说,有必要进行一些说明示教。此外,样本程序中使用的数据是一个虚构公司的员工的单个平面表格数据,因此处理这些复杂的表关系可以作为读者的一个练习项目。

本文中,我们将创建具有略微复杂的数据结构和 DataTables 浏览器的 Grails 应用程序。我们将介绍 Grails 标准,它是 Groovy 式的 Java Hibernate 标准。我已将代码托管在 GitHub 上方便大家访问,因此本文主要是对代码细节的解读。

首先,你需要配置 Java、Groovy、Grails 的使用环境。对于 Grails,我倾向于使用终端窗口和 Vim,本文也使用它们。为获得现代的 Java 环境,建议下载并安装 Linux 发行版提供的 Open Java Development Kit (OpenJDK)(应该是 Java 8、9、10 或 11 之一,撰写本文时,我正在使用 Java 8)。从我的角度来看,获取最新的 Groovy 和 Grails 的最佳方法是使用 SDKMAN!

从未尝试过 Grails 的读者可能需要做一些背景资料阅读。作为初学者,推荐文章 创建你的第一个 Grails 应用程序

获取员工信息浏览器应用程序

正如上文所提,我将本文中员工信息浏览器的源代码托管在 GitHub上。进一步讲,应用程序 embrow 是在 Linux 终端中用如下命令构建的:

cd Projects
grails create-app com.nuevaconsulting.embrow

域类和单元测试创建如下:

grails create-domain-class com.nuevaconsulting.embrow.Position
grails create-domain-class com.nuevaconsulting.embrow.Office
grails create-domain-class com.nuevaconsulting.embrow.Employeecd embrowgrails createdomaincom.grails createdomaincom.grails createdomaincom.

这种方式构建的域类没有属性,因此必须按如下方式编辑它们:

Position 域类:

package com.nuevaconsulting.embrow
 
class Position {

    String name
    int starting

    static constraints = {
        name nullable: false, blank: false
        starting nullable: false
    }
}com.Stringint startingstatic constraintsnullableblankstarting nullable

Office 域类:

package com.nuevaconsulting.embrow
 
class Office {

    String name
    String address
    String city
    String country

    static constraints = {
        name nullable: false, blank: false
        address nullable: false, blank: false
        city nullable: false, blank: false
        country nullable: false, blank: false
    }
}

Enployee 域类:

package com.nuevaconsulting.embrow
 
class Employee {

    String surname
    String givenNames
    Position position
    Office office
    int extension
    Date hired
    int salary
    static constraints = {
        surname nullable: false, blank: false
        givenNames nullable: false, blank: false
        : false
        office nullable: false
        extension nullable: false
        hired nullable: false
        salary nullable: false
    }
}

请注意,虽然 PositionOffice 域类使用了预定义的 Groovy 类型 String 以及 int,但 Employee 域类定义了 PositionOffice 字段(以及预定义的 Date)。这会导致创建数据库表,其中存储的 Employee 实例中包含了指向存储 PositionOffice 实例表的引用或者外键。

现在你可以生成控制器,视图,以及其他各种测试组件:

-all com.nuevaconsulting.embrow.Position
grails generate-all com.nuevaconsulting.embrow.Office
grails generate-all com.nuevaconsulting.embrow.Employeegrails generateall com.grails generateall com.grails generateall com.

此时,你已经准备好了一个基本的增删改查(CRUD)应用程序。我在 grails-app/init/com/nuevaconsulting/BootStrap.groovy 中包含了一些基础数据来填充表格。

如果你用如下命令来启动应用程序:

grails run-app

在浏览器输入 http://localhost:8080/,你将会看到如下界面:

Embrow home screen

Embrow 应用程序主界面。

单击 “OfficeController” 链接,会跳转到如下界面:

Office list

Office 列表

注意,此表由 OfficeControllerindex 方式生成,并由视图 office/index.gsp 显示。

同样,单击 “EmployeeController” 链接 跳转到如下界面:

Employee controller

employee 控制器

好吧,这很丑陋: Position 和 Office 链接是什么?

上面的命令 generate-all 生成的视图创建了一个叫 index.gsp 的文件,它使用 Grails <f:table/> 标签,该标签默认会显示类名(com.nuevaconsulting.embrow.Position)和持久化示例标识符(30)。这个操作可以自定义用来产生更好看的东西,并且自动生成链接,自动生成分页以及自动生成可排序列的一些非常简洁直观的东西。

但该员工信息浏览器功能也是有限的。例如,如果想查找 “position” 信息中包含 “dev” 的员工该怎么办?如果要组合排序,以姓氏为主排序关键字,“office” 为辅助排序关键字,该怎么办?或者,你需要将已排序的数据导出到电子表格或 PDF 文档以便通过电子邮件发送给无法访问浏览器的人,该怎么办?

jQuery DataTables 插件提供了这些所需的功能。允许你创建一个完成的表格数据浏览器。

创建员工信息浏览器视图和控制器的方法

要基于 jQuery DataTables 创建员工信息浏览器,你必须先完成以下两个任务:

  1. 创建 Grails 视图,其中包含启用 DataTable 所需的 HTML 和 JavaScript
  2. 给 Grails 控制器增加一个方法来控制新视图。

员工信息浏览器视图

在目录 embrow/grails-app/views/employee 中,首先复制 index.gsp 文件,重命名为 browser.gsp

cd Projects
cd embrow/grails-app/views/employee
cp gsp browser.gsp

此刻,你自定义新的 browser.gsp 文件来添加相关的 jQuery DataTables 代码。

通常,在可能的时候,我喜欢从内容提供商处获得 JavaScript 和 CSS;在下面这行后面:

<title><g:message code="default.list.label" args="[entityName]" /></title>

插入如下代码:

<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/scroller/1.4.4/css/scroller.dataTables.min.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/scroller/1.4.4/js/dataTables.scroller.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.flash.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.print.min.js "></script>

然后删除 index.gsp 中提供数据分页的代码:

<div id="list-employee" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<f:table collection="${employeeList}" />

<div class="pagination">
<g:paginate total="${employeeCount ?: 0}" />
</div>
</div>

并插入实现 jQuery DataTables 的代码。

要插入的第一部分是 HTML,它将创建浏览器的基本表格结构。DataTables 与后端通信的应用程序来说,它们只提供表格页眉和页脚;DataTables JavaScript 则负责表中内容。

<div id="employee-browser" class="content" role="main">
<h1>Employee Browser</h1>
<table id="employee_dt" class="display compact" style="width:99%;">
<thead>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</tfoot>
</table>
</div>

接下来,插入一个 JavaScript 块,它主要提供三个功能:它设置页脚中显示的文本框的大小,以进行列过滤,建立 DataTables 表模型,并创建一个处理程序来进行列过滤。

<g:javascript>
$('#employee_dt tfoot th').each( function() {javascript

下面的代码处理表格列底部的过滤器框的大小:

var title = $(this).text();
if (title == 'Extension' || title == 'Hired')
$(this).html('<input type="text" size="5" placeholder="' + title + '?" />');
else
$(this).html('<input type="text" size="15" placeholder="' + title + '?" />');
});titletitletitletitletitle

接下来,定义表模型。这是提供所有表选项的地方,包括界面的滚动,而不是分页,根据 DOM 字符串提供的装饰,将数据导出为 CSV 和其他格式的能力,以及建立与服务器的 AJAX 连接。 请注意,使用 Groovy GString 调用 Grails createLink() 的方法创建 URL,在 EmployeeController 中指向 browserLister 操作。同样有趣的是表格列的定义。此信息将发送到后端,后端查询数据库并返回相应的记录。

var table = $('#employee_dt').DataTable( {
"scrollY": 500,
"deferRender": true,
"scroller": true,
"dom": "Brtip",
"buttons": [ 'copy', 'csv', 'excel', 'pdf', 'print' ],
"processing": true,
"serverSide": true,
"ajax": {
"url": "${createLink(controller: 'employee', action: 'browserLister')}",
"type": "POST",
},
"columns": [
{ "data": "surname" },
{ "data": "givenNames" },
{ "data": "position" },
{ "data": "office" },
{ "data": "extension" },
{ "data": "hired" },
{ "data": "salary" }
]
});

最后,监视过滤器列以进行更改,并使用它们来应用过滤器。

table.columns().every(function() {
var that = this;
$('input', this.footer()).on('keyup change', function(e) {
if (that.search() != this.value && 8 < e.keyCode && e.keyCode < 32)
that.search(this.value).draw();
});

这就是 JavaScript,这样就完成了对视图代码的更改。

});
</g:javascript>

以下是此视图创建的UI的屏幕截图:

这是另一个屏幕截图,显示了过滤和多列排序(寻找 “position” 包括字符 “dev” 的员工,先按 “office” 排序,然后按姓氏排序):

这是另一个屏幕截图,显示单击 CSV 按钮时会发生什么:

最后,这是一个截图,显示在 LibreOffice 中打开的 CSV 数据:

好的,视图部分看起来非常简单;因此,控制器必须做所有繁重的工作,对吧? 让我们来看看……

控制器 browserLister 操作

回想一下,我们看到过这个字符串:

"${createLink(controller: 'employee', action: 'browserLister')}"

对于从 DataTables 模型中调用 AJAX 的 URL,是在 Grails 服务器上动态创建 HTML 链接,其 Grails 标记背后通过调用 createLink() 的方法实现的。这会最终产生一个指向 EmployeeController 的链接,位于:

embrow/grails-app/controllers/com/nuevaconsulting/embrow/EmployeeController.groovy

特别是控制器方法 browserLister()。我在代码中留了一些 print 语句,以便在运行时能够在终端看到中间结果。

    def browserLister() {
        // Applies filters and sorting to return a list of desired employees

首先,打印出传递给 browserLister() 的参数。我通常使用此代码开始构建控制器方法,以便我完全清楚我的控制器正在接收什么。

      println "employee browserLister params $params"
        println()

接下来,处理这些参数以使它们更加有用。首先,jQuery DataTables 参数,一个名为 jqdtParams 的 Groovy 映射:

def jqdtParams = [:]
params.each { key, value ->
    def keyFields = key.replace(']','').split(/\[/)
    def table = jqdtParams
    for (int f = 0; f < keyFields.size() - 1; f++) {
        def keyField = keyFields[f]
        if (!table.containsKey(keyField))
            table[keyField] = [:]
        table = table[keyField]
    }
    table[keyFields[-1]] = value
}
println "employee dataTableParams $jqdtParams"
println()

接下来,列数据,一个名为 columnMap 的 Groovy 映射:

def columnMap = jqdtParams.columns.collectEntries { k, v ->
    def whereTerm = null
    switch (v.data) {
    case 'extension':
    case 'hired':
    case 'salary':
        if (v.search.value ==~ /\d+(,\d+)*/)
            whereTerm = v.search.value.split(',').collect { it as Integer }
        break
    default:
        if (v.search.value ==~ /[A-Za-z0-9 ]+/)
            whereTerm = "%${v.search.value}%" as String
        break
    }
    [(v.data): [where: whereTerm]]
}
println "employee columnMap $columnMap"
println()

接下来,从 columnMap 中检索的所有列表,以及在视图中应如何排序这些列表,Groovy 列表分别称为 allColumnListorderList

def allColumnList = columnMap.keySet() as List
println "employee allColumnList $allColumnList"
def orderList = jqdtParams.order.collect { k, v -> [allColumnList[v.column as Integer], v.dir] }
println "employee orderList $orderList"

我们将使用 Grails 的 Hibernate 标准实现来实际选择要显示的元素以及它们的排序和分页。标准要求过滤器关闭;在大多数示例中,这是作为标准实例本身的创建的一部分给出的,但是在这里我们预先定义过滤器闭包。请注意,在这种情况下,“date hired” 过滤器的相对复杂的解释被视为一年并应用于建立日期范围,并使用 createAlias 以允许我们进入相关类别 PositionOffice

def filterer = {
    createAlias 'position',        'p'
    createAlias 'office',          'o'

    if (columnMap.surname.where)    ilike  'surname',     columnMap.surname.where
    if (columnMap.givenNames.where) ilike  'givenNames',  columnMap.givenNames.where
    if (columnMap.position.where)   ilike  'p.name',      columnMap.position.where
    if (columnMap.office.where)     ilike  'o.name',      columnMap.office.where
    if (columnMap.extension.where)  inList 'extension',   columnMap.extension.where
    if (columnMap.salary.where)     inList 'salary',      columnMap.salary.where
    if (columnMap.hired.where) {
        if (columnMap.hired.where.size() > 1) {
            or {
                columnMap.hired.where.each {
                    between 'hired', Date.parse('yyyy/MM/dd',"${it}/01/01" as String),
                        Date.parse('yyyy/MM/dd',"${it}/12/31" as String)
                }
            }
        } else {
            between 'hired', Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/01/01" as String),
                Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/12/31" as String)
        }
    }
}

是时候应用上述内容了。第一步是获取分页代码所需的所有 Employee 实例的总数:

        def recordsTotal = Employee.count()
        println "employee recordsTotal $recordsTotal"

接下来,将过滤器应用于 Employee 实例以获取过滤结果的计数,该结果将始终小于或等于总数(同样,这是针对分页代码):

        def c = Employee.createCriteria()
        def recordsFiltered = c.count {
            filterer.delegate = delegate
            filterer()
        }
        println "employee recordsFiltered $recordsFiltered"

获得这两个计数后,你还可以使用分页和排序信息获取实际过滤的实例。

      def orderer = Employee.withCriteria {
            filterer.delegate = delegate
            filterer()
            orderList.each { oi ->
                switch (oi[0]) {
                case 'surname':    order 'surname',    oi[1]; break
                case 'givenNames': order 'givenNames', oi[1]; break
                case 'position':   order 'p.name',     oi[1]; break
                case 'office':     order 'o.name',     oi[1]; break
                case 'extension':  order 'extension',  oi[1]; break
                case 'hired':      order 'hired',      oi[1]; break
                case 'salary':     order 'salary',     oi[1]; break
                }
            }
            maxResults (jqdtParams.length as Integer)
            firstResult (jqdtParams.start as Integer)
        }

要完全清楚,JTable 中的分页代码管理三个计数:数据集中的记录总数,应用过滤器后得到的数字,以及要在页面上显示的数字(显示是滚动还是分页)。 排序应用于所有过滤的记录,并且分页应用于那些过滤的记录的块以用于显示目的。

接下来,处理命令返回的结果,在每行中创建指向 EmployeePositionOffice 实例的链接,以便用户可以单击这些链接以获取相关实例的所有详细信息:

        def dollarFormatter = new DecimalFormat('$##,###.##')
        def employees = orderer.collect { employee ->
            ['surname': "<a href='${createLink(controller: 'employee', action: 'show', id: employee.id)}'>${employee.surname}</a>",
                'givenNames': employee.givenNames,
                'position': "<a href='${createLink(controller: 'position', action: 'show', id: employee.position?.id)}'>${employee.position?.name}</a>",
                'office': "<a href='${createLink(controller: 'office', action: 'show', id: employee.office?.id)}'>${employee.office?.name}</a>",
                'extension': employee.extension,
                'hired': employee.hired.format('yyyy/MM/dd'),
                'salary': dollarFormatter.format(employee.salary)]
        }

最后,创建要返回的结果并将其作为 JSON 返回,这是 jQuery DataTables 所需要的。

        def result = [draw: jqdtParams.draw, recordsTotal: recordsTotal, recordsFiltered: recordsFiltered, data: employees]
        render(result as JSON)
    }

大功告成。

如果你熟悉 Grails,这可能看起来比你原先想象的要多,但这里没有火箭式的一步到位方法,只是很多分散的操作步骤。但是,如果你没有太多接触 Grails(或 Groovy),那么需要了解很多新东西 - 闭包,代理和构建器等等。

在那种情况下,从哪里开始? 最好的地方是了解 Groovy 本身,尤其是 Groovy closuresGroovy delegates and builders。然后再去阅读上面关于 Grails 和 Hibernate 条件查询的建议阅读文章。

结语

jQuery DataTables 为 Grails 制作了很棒的表格数据浏览器。对视图进行编码并不是太棘手,但 DataTables 文档中提供的 PHP 示例提供的功能仅到此位置。特别是,它们不是用 Grails 程序员编写的,也不包含探索使用引用其他类(实质上是查找表)的元素的更精细的细节。

我使用这种方法制作了几个数据浏览器,允许用户选择要查看和累积记录计数的列,或者只是浏览数据。即使在相对适度的 VPS 上的百万行表中,性能也很好。

一个警告:我偶然发现了 Grails 中暴露的各种 Hibernate 标准机制的一些问题(请参阅我的其他 GitHub 代码库),因此需要谨慎和实验。如果所有其他方法都失败了,另一种方法是动态构建 SQL 字符串并执行它们。在撰写本文时,我更喜欢使用 Grails 标准,除非我遇到杂乱的子查询,但这可能只反映了我在 Hibernate 中对子查询的相对缺乏经验。

我希望 Grails 程序员发现本文的有趣性。请随时在下面留下评论或建议。


via: https://opensource.com/article/18/9/using-grails-jquery-and-datatables

作者:Chris Hermansen 选题:lujun9972 译者:jrg 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出