Groovy学习篇章一之—— GDK 探秘:Groovy如何给Java对象“开外挂”,让String也能“跑命令”!

Groovy学习

一 介绍

Groovy第一个版本发布于2007年,是一种用于Java虚拟机的敏捷地动态语言,同时也是一门面向对象语言。即可以作为编程语言,也可以作为纯粹的脚本语言。

动态语言与静态语言:

动态语言 ,是在运行时确定数据类型的语言,变量在使用前无需声明类型,类型会根据赋值动态推断。例如,Python、JavaScript 和 PHP 都属于动态语言。动态语言的特点是灵活性高,开发者可以专注于业务逻辑,而不需要过多关注类型声明。

静态语言 ,则是在编译时确定数据类型的语言。变量在使用前必须显式声明类型,且类型在程序生命周期内通常不会改变。例如,C、C++ 和 Java 都是静态语言。静态语言的优势在于编译器可以在编译阶段捕获类型错误,从而提高代码的安全性和稳定性。

二 Groovy与Java的关系

  1. java代码文件修改后缀为.groovy后,可以直接作为groovy文件运行,即使其内部的代码仍为java代码,这体现了groovy与java语言本身的互通性。
  1. 当我们对demo01.groovy文件中的代码进行简化,去掉类名、方法体等之后,其仍能得到与java一样的运行结果,这体现了groovy语言相较于java语言的简洁性和轻量性。

三 GDK

和Java相似,Java有JDK,Groovy也有自己的GDK,但GDK其实是依赖于JDK的,因为其是在不修改 JDK源码的情况下,通过MetaClass 和 扩展模块(Extension Modules) 机制,在运行时为 JDK 的类"注入"了额外的功能,故可以说Groovy扩展了JDK,这些扩展形成了GDK(Groovy JDK)。

1. GDKJDK的扩展

举一个例子:

在java中,当我们需要与系统进行交互时,我们可以使用java.lang.process包下的类来进行,比如说在控制台打印执行"git help"命令的内容,java实现如下:

java 复制代码
public class Main {
    public static void main(String[] args) throws IOException {
        Process process = Runtime.getRuntime().exec("git help");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }
        bufferedReader.close();
    }
}
​
//执行结果如下:
/*
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--config-env=<name>=<envvar>] <command> [<args>]
​
These are common Git commands used in various situations:
​
start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one
​
work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index
​
examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status
​
grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG
​
collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects
​
'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.
*/

而在groovy中,只需要一句即可实现同java一样的效果:

Groovy 复制代码
println "git help".execute().text
//执行结果同样为:
/*
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--config-env=<name>=<envvar>] <command> [<args>]
​
These are common Git commands used in various situations:
​
start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one
​
work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index
​
examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status
​
grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG
​
collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects
​
'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.
*/
复制代码
这是为什么呢?让我们进入GDK的源码进行查看

(1).按住ctrl,鼠标单击 execute()方法

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

(2).按住ctrl,鼠标单击 text :

java 复制代码
    public static String getText(Process self) throws IOException {
        String text = IOGroovyMethods.getText(new BufferedReader(new InputStreamReader(self.getInputStream())));
        closeStreams(self);
        return text;
    }

上面两个方法都存在于ProcessGroovyMethods类中的。

可以看到,GDK的源码是Java编写的。

前面说过Groovy是面向对象的编程语言,那么"git help".execute()的含义应该是调用String类中的execute()方法,但是我们进入源码查看却发现,String类中并不存在execute()方法,且GDK 中的String类源码和JDK中的String源码是一模一样的,这就验证了Groovy是没有修改JDK源码的。

但GDK中的String类并不存在execute()方法,为何我们编译时不会报错,运行时也不会报错呢?首先, 因为Groovy是一门动态语言,故其在编译时并不会严格检查当前对象是否能调用这个方法;其次, Groovy拥有**MetaClass(元类)** 和扩展模块机制

MetaClass(元类)是 Groovy 中一个核心的运行时机制,可以理解为每个 Groovy 对象都附带的一个"行为控制器"或"方法查找器",且MetaClass会知晓其对应的类中所有的信息(即JDK中的定义的方法和属性)。

扩展模块(比如 DefaultGroovyMethods)中定义的静态方法,在运行时会被注册到对应类(需要它的类)的 MetaClass 中,从而让这些静态方法看起来就像是原始类的实例方法一样。

故而,在执行"git help".execute()代码时,就可以理解为是Groovy在运行时动态地将ProcessGroovyMethods类中的方法装载到了String类对应的MetaClass中,使String类"看起来"拥有了execute()方法,故而不会报错,且可正确执行,最终返回一个Process对象。

同理,可以理解,在执行.text代码时,虽然Process类中并不存在getText()方法,但通过Groovy的**MetaClass 和扩展模块机制** ,可以在运行时动态地将ProcessGroovyMethods类中的方法装载到了 Process类MetaClass中,最终就可以得到我们需要的 字符串结果。

最后通过 println 方法,将结果打印出来。

从上面,这个例子,就可以看出,GDK中将一些常用的功能提前封装为一些模块中的方法(如ProcessGroovyMethods类中的getText(Process self)方法,将java中的读取输入流中的字符串和关闭流提前一起封装了),通过**MetaClass(元类)和扩展模块机制**,在运行时实现为某些类动态扩展一些方法,使得Groovy的代码的编写更为简洁。

2. 扩展模块(类)的内部机制

那当Groovy在运行时是如何知道要将扩展模块中的每个方法扩展到哪个到对应的类的MetaClass中的呢?

例如:如何知道要将ProcessGroovyMethods类中的getText(Process self)方法需要在运行时扩展到String类的MetaClass

1.什么是扩展模块

Groovy中的"扩展模块"本质上是 Groovy 运行时默认加载的静态方法集合 ,它们通过约定静态方法的第一个参数类型就是被扩展的类 ,Groovy 运行时会根据这个约定将方法注入到被扩展的类的**MetaClass(元类)**中。

也就是说,我们上面说的ProcessGroovyMethods类就可视为Groovy中的一个扩展模块,其中的方法:

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

其中第一个参数类型为String,Groovy在运行时就会将此方法注册添加到String类的MetaClass中。

在Groovy中,当我们调用方法时,都会到其对应的 metaClass 保存的信息中查找,如果找到就会调用。故当执行String对象.execute(String self)时,Groovy会到String 类的 MetaClass 类中查找并调用execute(String self)方法。

2. GDK 中的主要扩展模块(类)

DefaultGroovyMethods (DGM):

  • 作用: 这是 GDK 中最核心、最庞大 的扩展类。它为几乎所有基本的 Java 类型(如 ObjectStringIntegerListMapFileDate 等)提供了大量的通用方法。

  • 例子:

    • "hello".each { println it } (为 String 添加 each 迭代方法)

    • [1, 2, 3].collect { it * 2 } (为 List 添加 collect 转换方法)

    • new File("test.txt").text = "Hello" (为 File 添加 text 属性读写)

    • 1.plus(2) (为 Integer 添加 plus 方法,虽然通常直接用 +)

StringGroovyMethods

  • 作用: 专门为 java.lang.String 类提供了更多字符串操作的便捷方法。虽然很多字符串方法也在 DGM 中,但这个类会包含一些更专业的字符串处理。

  • 例子:

    • "hello".capitalize() (将首字母大写)

    • "foo".padLeft(5, '*') (左侧填充)

ProcessGroovyMethods

  • 作用: 正如我们之前讨论的,它专门为 java.lang.Process 类提供了增强方法,简化了外部进程的输入输出处理和管理。

  • 例子:

    • "ls -l".execute().text (执行命令并直接获取输出文本)

    • process.waitForProcessOutput() (等待进程输出并避免阻塞)

IOGroovyMethods

  • 作用:java.io.Filejava.io.InputStreamjava.io.OutputStream 等I/O相关的类提供了大量简化文件和流操作的方法。

  • 例子:

    • new File("data.txt").eachLine { line -> println line } (按行读取文件)

    • inputStream.eachByte { byte -> ... } (按字节处理流)

DateGroovyMethods / TimeCategory

  • 作用: 增强了 java.util.Datejava.util.Calendar 等日期时间类,提供了更直观的日期时间计算和操作方法。

  • 例子:

    • (new Date()) + 1.day (日期加一天)

    • use(TimeCategory) { 1.hour.from.now } (更灵活的时间计算)

相关推荐
Boilermaker19921 分钟前
【Java EE】Spring AOP
java·java-ee
网安Ruler12 分钟前
Web开发-PHP应用&文件操作安全&上传下载&任意读取删除&目录遍历&文件包含
android
图灵学术计算机论文辅导24 分钟前
从 “看懂图” 到 “读懂视频”:多模态技术如何用文本反哺视觉?
论文阅读·人工智能·学习·考研·计算机视觉·目标跟踪
Code blocks26 分钟前
SpringBoot中策略模式使用
java·spring boot·后端·mybatis·策略模式
aningxiaoxixi26 分钟前
android audio 之 Engine
android·前端·javascript
教程分享大师32 分钟前
带root_兆能ZN802及兆能ZNM802融合终端安卓9系统线刷机包 当贝纯净版
android·电脑
污领巾33 分钟前
虚幻GAS底层原理解剖三 (GA)
java·游戏引擎·虚幻
C4程序员41 分钟前
北京JAVA基础面试30天打卡02
java·开发语言·面试
好好研究1 小时前
Java基础学习(一):类名规范、返回值、注释、数据类型
java·学习·算法
_码农121381 小时前
java web 未完成项目,本来想做个超市管理系统,前端技术还没学。前端是个简单的html。后端接口比较完善。
java·前端·html