初识 Groovy

简介

Groovy由James Strachan于2003年创建,它的目标是提供一个与Java紧密集成、更灵活、更简洁的编程语言,成为一种功能丰富且对 Java 友好的语言,将动态语言的优势引入一个强大且支持良好的平台。

作为一种运行在Java虚拟机上的脚本语言,Groovy以其动态性和与Java语言的高度集成而著称。它支持动态类型、闭包、简洁语法等特性,使开发者能够更高效地编写代码。在总体上,Java非常适合用于工具、库和基础设施,而Groovy则非常适合于几乎所有其他用途。

与Java的关系

  • 兼容性:Groovy与Java高度兼容,可以直接访问Java类和库。

  • 互操作性:Java可以调用Groovy代码,反之亦然。

    编译过程

主要语法特性:

  1. 动态类型。
  2. 结束不需要分号。
  3. 可选 return 关键字,默认最后一个变量作为返回值。

应用场景

  • 脚本编写
  • 测试框架
  • Gradle构建工具
  • Web开发
  • DSL(领域设计语言)

第一个程序

  1. 通过 Maven引入;或者,通过安装 Groovy 的二进制发行版:https://groovy.apache.org/download.html
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.5.14</version>
</dependency>
  1. 编写 Demo.groovy
// Demo.groovy
class Demo {
    static void main(String[] args) {
      	// 打印 hello world!
        println "hello world!"
    }
}

语法对比

  1. 动态类型。与 Java 相比,Java 是一种"强"类型语言,编译器知道每个变量的所有类型,并且可以在编译时理解和遵守合同。在 Groovy 中,可选类型是通过 def 关键字完成的。
// Example.groovy
class Example { 
   static void main(String[] args) { 
      // 使用 def 的整数示例
      def aint = 100; 
      println(aint); 
   } 
}      
  1. true 的判断。所有 null、void、等于零或 empty 的对象都计算为 false
if (name != null && name.length > 0) {}
// 等价于
if (name) {}
  1. == 的作用。在 Groovy 中, == 意味着所有地方都相等。对于非基元,当评估Comparable对象的相等性时,它会转换为 a.compareTo(b) == 0 ,否则转换为 a.equals(b) 。要检查同一性(引用相等),请使用 is 方法: a.is(b)
status != null && status.equals(ControlConstants.STATUS_COMPLETED)
// 等价于
status == ControlConstants.STATUS_COMPLETED
  1. 有用的语法糖。
    • ?. 安全航导运算符
    • ?: Elvis 运算符;
// Groovy引入了安全导航运算符 ?.,可以避免空指针异常的发生。
if (order != null) {
    if (order.getCustomer() != null) {
        if (order.getCustomer().getAddress() != null) {
            System.out.println(order.getCustomer().getAddress());
        }
    }
}
// 等价于
println order?.customer?.address

// Elvis 运算符是一种特殊的三元运算符快捷方式,可以方便地用于默认值。
def result = name != null ? name : "Unknown"
// 等价于
def result = name ?: "Unknown"
  1. getter 和 setter。在 Groovy 中,getter 和 setter 构成了我们所说的“属性”,并提供了访问和设置此类属性的快捷方式表示法;在 Groovy 中编写 bean时,您不必自己创建字段和 getter / setter,而是让 Groovy 编译器为您完成。
// 在 Groovy 中,getter 和 setter 构成了我们所说的“属性”,并提供了访问和设置此类属性的快捷方式表示法
resourcePrototype.setName("something")
// 等价于
resourcePrototype.name = "something"

class Person {
    private String name
    String getName() { return name }
    void setName(String name) { this.name = name }
}
// 等价于
class Person {
    String name
}
  1. 使用 with()tap() 对同一个bean 进行重复操作。
server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()
// 等价于
server.with {
    name = application.name
    status = status
    sessionCount = 3
    start()
    stop() // 闭包中,最后一条语句视为返回值。
}

def person = new Person().with {
    name = "Ada Lovelace"
    it // 返回本身
}
// 等价于
def person = new Person().tap {
    name = "Ada Lovelace"
}

注意:您还可以使用 with(true) 代替 tap(),使用 with(false) 代替 with()。

  1. 其他。

简单应用

使用如下Groovy类库,从Java中调用Groovy脚本:

  1. Eval:最简单集成的方法
  2. GroovyShell:运行脚本的首选方法
    1. Script:自定义脚本类
  3. GroovyClassLoader:Groovy类加载器
  4. GroovyScriptEngine:Groovy脚本引擎

此外,还可以使用 JSR 223,从Java中调用Groovy脚本。

Eval、GroovyShell

groovy.util.Eval 类是在运行时动态执行 Groovy 的最简单方法。这可以通过调用 me 方法来完成:

// Groovy 默认导入包:groovy.util.*
// import groovy.util.Eval

class EvalDemo {
    static void main(String[] args) {
        assert Eval.me("33*3") == 99
    }
}

Eval支持多种接受参数进行简单判定的变体:

class EvalDemo {
    static void main(String[] args) {
        // 使用名为x绑定参数进行简单判定
        assert Eval.x(4, '2*x') == 8
        // 相同的判定,使用名为k自定义绑定参数
        assert Eval.me('k', 4, '2*k') == 8
        // 使用名为x和y两个绑定参数进行简单判定
        assert Eval.xy(4, 5, 'x*y') == 20
        // 使用名为x 、 y和z三个绑定参数进行简单判定
        assert Eval.xyz(4, 5, 6, 'x*y+z') == 26
    }
}

groovy.lang.GroovyShell类是执行脚本的首选方法,并且能够缓存生成的脚本实例。虽然Eval类返回编译脚本的执行结果,但GroovyShell类提供了更多选项。

// GroovyShellDemo.groovy
class GroovyShellDemo {
    static void main(String[] args) {
        // 创建一个新的GroovyShell实例
        def shell = new GroovyShell()
        // 可以用作直接执行代码的Eval
        def result = shell.evaluate '3*5'
        def result2 = shell.evaluate(new StringReader('3*5'))
        assert result == result2
        def script = shell.parse '3*5'
        assert script instanceof groovy.lang.Script
        assert script.run() == 15
    }
}

等价于Java代码

import groovy.lang.Script;

import java.io.StringReader;

public class GroovyShellDemo {
    public static void main(String[] args) {
        // 创建一个新的GroovyShell实例
        GroovyShell shell = new GroovyShell();
        // 可以用作直接执行代码的Eval
        Object result = shell.evaluate( "3*5");
        Object result2 = shell.evaluate(new StringReader("3*5"));
        assert result == result2;
        Script script = shell.parse("3*5");
        assert script instanceof groovy.lang.Script;
        assert script.run().equals(15);
    }
}

可以使用groovy.lang.Binding在应用程序和脚本之间共享数据:

// 创建一个包含共享数据的新Binding
def sharedData = new Binding()                          
def shell = new GroovyShell(sharedData)                 
def now = new Date()
// 将字符串添加到绑定中
sharedData.setProperty('text', 'I am shared data!')   
// 向绑定添加日期(不限于简单类型)
sharedData.setProperty('date', now)                     

String result = shell.evaluate('"At $date, $text"')     

assert result == "At $now, I am shared data!"

同样,可以读取脚本绑定的值:

def sharedData = new Binding()                          
def shell = new GroovyShell(sharedData)                 

shell.evaluate('foo=123')                               
// 读取调用者的结果
assert sharedData.getProperty('foo') == 123  

在多线程环境中使用共享数据时必须非常小心。您传递给GroovyShellBinding实例不是线程安全的,并且由所有脚本共享。

GroovyShell 流程示意图

Script

我们已经看到parse方法返回groovy.lang.Script的实例,但可以使用自定义类,因为它扩展了Script本身。它可用于为脚本提供附加行为,如下例所示:

abstract class MyScript extends Script {
    String name

    String greet() {
        "Hello, $name!"
    }
}

GroovyClassLoader

我们已经展示了GroovyShell是一个执行脚本的简单工具,但它使得编译除脚本之外的任何内容都变得复杂。在内部,它使用groovy.lang.GroovyClassLoader ,它是运行时编译和加载类的核心。

通过利用GroovyClassLoader而不是GroovyShell ,您将能够加载类,而不是脚本实例:

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()                                           
def clazz = gcl.parseClass('class Foo { void doIt() { println "ok" } }')    
assert clazz.name == 'Foo'                                                  
def o = clazz.newInstance()                                                 
o.doIt() 

GroovyClassLoader 保留它创建的所有类的引用,因此很容易造成内存泄漏。特别是,如果您执行相同的脚本两次,如果它是一个字符串,那么您将获得两个不同的类!

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()
// 使用单独的parseClass调用创建一个外观相同的类
def clazz1 = gcl.parseClass('class Foo { }')                             
def clazz2 = gcl.parseClass('class Foo { }')                             

assert clazz1.name == 'Foo'                                               
assert clazz2.name == 'Foo'
// 但它们实际上是不同的!
assert clazz1 != clazz2                                                   

原因是GroovyClassLoader不跟踪源文本。如果您想拥有相同的实例,那么源必须是一个文件,如下例所示:

def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass(file)    
// 从不同的文件实例解析类,但指向相同的物理文件
def clazz2 = gcl.parseClass(new File(file.absolutePath))                    
assert clazz1.name == 'Foo'                                                 
assert clazz2.name == 'Foo'
assert clazz1 == clazz2   

GroovyScriptEngine

groovy.util.GroovyScriptEngine类为依赖脚本重新加载和脚本依赖项的应用程序提供了灵活的基础。虽然GroovyShell专注于独立Scripts and GroovyClassLoader处理任何 Groovy 类的动态编译和加载,但GroovyScriptEngine将在GroovyClassLoader之上添加一个层来处理脚本依赖项和重新加载。

class Greeter {
    String sayHello() {
        def greet = "Hello, world!"
        greet
    }
}

new Greeter()
def binding = new Binding()
def engine = new GroovyScriptEngine([tmpDir.toURI().toURL()] as URL[])          

while (true) {
    def greeter = engine.run('ReloadingTest.groovy', binding)                   
    println greeter.sayHello()                                                  
    Thread.sleep(1000)
}

JSR-223脚本:javax.script API

JSR-223 是用于调用 Java 脚本框架的标准 API。它从 Java 6 开始可用,旨在提供一个从 Java 调用多种语言的通用框架。 Groovy 提供了自己更丰富的集成机制,如果您不打算在同一应用程序中使用多种语言,建议您使用 Groovy 集成机制而不是有限的 JSR-223 API。

JSR 223 流程示意图

以下是初始化 JSR-223 引擎以从 Java 与 Groovy 对话的方法:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");

然后您可以轻松执行 Groovy 脚本:

Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(Integer.valueOf(55), sum);

也可以共享变量:

engine.put("first", "HELLO");
engine.put("second", "world");
String result = (String) engine.eval("first.toLowerCase() + ' ' + second.toUpperCase()");
assertEquals("hello WORLD", result);

下一个示例说明了调用可调用函数:

import javax.script.Invocable;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
String fact = "def factorial(n) { n == 1 ? 1 : n * factorial(n - 1) }";
engine.eval(fact);
Invocable inv = (Invocable) engine;
Object[] params = {5};
Object result = inv.invokeFunction("factorial", params);
assertEquals(Integer.valueOf(120), result);

引擎默认保留对脚本函数的硬引用。要更改此设置,您应该将引擎级作用域属性设置为名称#jsr223.groovy.engine.keep.globals的脚本上下文,其中字符串为phantom以使用幻像引用, weak为使用弱引用或soft为使用软引用 -大小写被忽略。任何其他字符串都会导致使用硬引用。

进阶应用

案例:动态编程

定义业务接口,通过Groovy脚本实现动态编程。可以将脚本存放到指定路径或者数据库中

  1. 定义业务接口
package cn.yujian95.learn.groovy.java;

import java.util.List;

/**
 * @author yujian yujian95_cn@163.com
 */
public interface IBizFilter {
    // 过滤逻辑
    List<String> filter(List<String> list);
}
  1. 编写Groovy脚本,实现接口编写业务逻辑
package cn.yujian95.learn.groovy.java

import java.util.stream.Collectors
// 必须显示导入接口
import cn.yujian95.learn.groovy.java.IBizFilter

class BizFilterStrategyA implements IBizFilter {

    @Override
    List<String> filter(List<String> list) {
        return list.stream().filter({ o -> ("A" == o) }).collect(Collectors.toList())
    }
}

  1. 编写Groovy工具类,编译脚本实例
package cn.yujian95.learn.groovy.java;

import cn.hutool.core.lang.Assert;
import groovy.lang.GroovyClassLoader;
import lombok.extern.slf4j.Slf4j;

/**
 * @author yujian yujian95_cn@163.com
 */
@Slf4j
public class GroovyUtil {
    private static final GroovyClassLoader classLoader = new GroovyClassLoader(BizFilterDemo.class.getClassLoader());

    public static IBizFilter compile(String classScript) {
        Assert.notEmpty(classScript, "脚本不能为空!");
        Object newInstance = null;

        try {
            Class groovyClass = classLoader.parseClass(classScript);
            newInstance = groovyClass.newInstance();

            if (!(newInstance instanceof IBizFilter)) {
                // 错误处理
                log.error("未实现接口");
                return null;
            }
        } catch (Exception exception) {
            // 异常处理
            log.error("异常信息:{}" , exception.getMessage());
        }

        if (newInstance == null) {
            return null;
        }
				
      	// 将实例转换为接口
        return IBizFilter.class.cast(newInstance);
    }
}
  1. 读取Groovy脚本,并执行业务逻辑
package cn.yujian95.learn.groovy.java;


import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * @author yujian yujian95_cn@163.com
 */
@Slf4j
public class BizFilterDemo {

    public static List<String> initTestData() {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        return list;
    }

    public static void main(String[] args) {
        // 读取脚本内容
        IBizFilter iBizFilter = GroovyUtil.compile("import java.util.stream.Collectors\n" +
                "import cn.yujian95.learn.groovy.java.IBizFilter\n" +
                "\n" +
                "class BizFilterStrategyA implements IBizFilter {\n" +
                "\n" +
                "    BizFilterStrategyA() {}\n" +
                "\n" +
                "    @Override\n" +
                "    List<String> filter(List<String> list) {\n" +
                "        return list.stream().filter({ o -> (\"A\" == o) }).collect(Collectors.toList())\n" +
                "    }\n" +
                "}");
        if (iBizFilter != null) {
            System.out.println(iBizFilter.filter(initTestData()));
        }
    }
}

扩展:安全性优化(WIP)

总结

通过学习Groovy,我们可以为自己的开发技能增加新的元素和工具,提高我们的编程效率和代码质量,希望这篇文章能够帮助大家更好地了解和应用Groovy这个强大的编程语言。

相关学习资料: