groovy学习

Posted by Shi Hai's Blog on March 1, 2023

主要想通过Groovy来管理架构的DSL,实际用DSL的逻辑组织会强于json、xml这类文件存储格式,当然带来的成本还是需要熟悉一下groovy的语法。主要还是对官网给的一些示例代码熟悉一下,主要重点想看一下怎么定义DSL和存储格式(ToString、yaml、json、xml等)的输出。 groovy吸收了很多Java和Python的语言特性,代码本身应该用vim编辑是比较高效的,不过我刚起步学习,先用Intellij找找感觉。

一、闭包

闭包的语法定义:{ [closureParameters -> ] statements },其中[closureParameters -> ]是可选择的。

1.1 基本闭包用法

闭包实际是一个匿名代码段。

class Example {
   static void main(String[] args) {
      def clos = {println "Hello World"};
      clos.call();
   } 
}

1.2 闭包作为参数

List、Map和String接收闭包作为参数。

class Example {
   static void main(String[] args) {
      def numbers = [1, 2, 3, 44];
      numbers.each {println it}
   } 
}

1.3 通过闭包实现函数式编程

具体实现如下所示,其中it->println it是一个lambda表达式

class Fp {
    static foreach(nums, action) {
        for (i in nums) {
            action(i)
        }
    }
    static void main(String[] args) {
        Fp.foreach(1..10, {it->println it})
    }
}

1.4 Closure对象的使用

def listener = { e -> println "Clicked on $e.source"}
assert listener instanceof Closure
Closure callback = { println "Hello World!"}
callback.call()
// 使用Closure对象,可以指定闭包返回类型
Closure<Boolean> isTextFile = { File it -> it.name.endsWith('.txt')}

1.5 闭包委托

闭包有三个基本属性:thisobjectownerdelegate。所有的闭包都和它所在类的实例绑定。并且闭包会被groovy编译成一个内部类的实例。对于一般像下面示例中的out闭包存在thisobject=owner=delegate的关系。

class Example1 {
    def out = { // 这是外部闭包
        def inner = { // 这是out内部闭包
        }

        println out.thisObject.getClass().name
        println out.owner.getClass().name
        println out.delegate.getClass().name

        println inner.thisObject.getClass().name
        println inner.owner.getClass().name
        println inner.delegate.getClass().name
    }
}

上面示例的输出结果为:

Example1
Example1
Example1
Example1
Example1$_closure1
Example1$_closure1

下面的代码示例展示的是闭包委托的实际执行逻辑:

class Proxy {
    def func = { println "Proxy's func()" }
}

class Example{

    def func = { println "Example's func()"}
    def out = {
        def inner = {
            func()
        }
        inner.delegate = new Proxy()
        inner()
    }
}

new Example().out()

输出结果为:

Example's func()

而当把Example.func()注释掉后,则会打印Proxy.func()中的内容。这是因为闭包会优先从thisobjectowner中选择,如果没有才会到delegate中查找需要调用的内容。

class Proxy {
    def func = { println "Proxy's func()" }
}

class Example{

    def func = { println "Example's func()"}
    def out = {
        def inner = {
            func()
        }
        inner.delegate = new Proxy()
        // 补充添加这一行
        new Proxy().with inner
    }
}

new Example().out()

则输出为:Proxy's func()。这是因为将inner()闭包通过with()函数传给对象new Proxy(),让对象自己可以调用到闭包。

一个嵌套类的调用问题

class Outer {
    static void out() {
        System.out.println("hello world");
    }
    static class Inner {
    }

    static void main(String[] args) {
        //Intellij Note: No candidates found for method call
        Outer.Inner.out();
    }
}

这段代码在java中肯定会报错,但是groovy里面是执行成功的。还没找到具体原因,但官网有句对嵌套类实现的描述,我个人猜测和groovy中的嵌套类是依赖闭包特性实现有关 详情

The implementation of anonymous inner classes and nested classes follow Java closely, but there are some differences, e.g. local variables accessed from within such classes don’t have to be final. We piggyback on some implementation details we use for groovy.lang.Closure when generating inner class bytecode.

在stackoverflow提了个问题,坐等其他人给答案。

二、注解

2.1 ToString注解

此注解指示编译器在做AST编译时能生成toString()函数,示例代码如下所示:

package test

import groovy.transform.ToString

@ToString(includeNames=true)
class Test {
    String first, last
    int age

    static void main(String[] args) {
        println new Test(first:"first_name", last:"last_name", age: 20)
    }
}

执行示例代码后得到的输出结果:

Customer(first:first_name, last:null, age:10, since:Fri Mar 03 10:57:41 CST 2023, favItems:null)

2.2 Builder注解

@Builder 注解能帮助我们更加高效的构建出一个类。

package test

import groovy.transform.builder.Builder
import groovy.transform.builder.SimpleStrategy

@Builder(builderStrategy = SimpleStrategy)
class Person {
    String firstName
    String lastName
    int age

    static void main(String[] args) {
        def person = new Person().setFirstName("Robert").setLastName("Lewa").setAge(21)
        println person.firstName
        println(person.lastName)
        println(person.age)
    }
}

执行测试代码后的输出结果如下所示:

Robert
Lewa
21

Builder里面提供了很多的构建策略,如果你想扩建自己的构建策略也是没问题的。prefix注解参数可以用来创建不同名称的属性设置函数,我们可以按需修改,示例代码如下所示:

package test

import groovy.transform.builder.Builder
import groovy.transform.builder.SimpleStrategy

@Builder(builderStrategy = SimpleStrategy, prefix="with")
class Person {
    String firstName
    String lastName
    int age

    static void main(String[] args) {
        def Person anotherPerson = new Person().withFirstName("Mark").withLastName("Shan").withAge(20)
        println(anotherPerson.firstName)
        println(anotherPerson.lastName)
        println(anotherPerson.age)
    }
}

执行此示例代码的结果输出如下所示:

Mark
Shan
20

2.3 DelegatesTo注解

API或者DSL编写者可以使用此注解来接收闭包作为参数,并指定闭包的委托类型,这样在IDE中使用闭包时会有提示功能。另外,这个注解也用来帮助类型检查器检查传入参数的类型。下面是一个闭包委托的示例代码:

package test

import org.codehaus.groovy.runtime.DefaultGroovyMethods

class Conf {
    String name
    int age

    def name(String name) {
        this.name = name
        return this
    }

    def age(int age) {
        this.age = age
        return this
    }

    def static user(@DelegatesTo(Conf.class) Closure<Conf> closure) {
        Conf conf = new Conf()
        // 容许对象(conf)调用闭包
        DefaultGroovyMethods.with(conf, closure)
        println(conf.name)
        println(conf.age)
    }

    static void main(String[] args) {
        user {
            name "test"
            age 12
        }
    }
}

执行此示例代码后的输出为:

test
12

此示例代码还可以简化为:

package model.principle

import groovy.transform.builder.Builder
import groovy.transform.builder.SimpleStrategy
import org.codehaus.groovy.runtime.DefaultGroovyMethods

@Builder(builderStrategy = SimpleStrategy, prefix="with")
class Conf {
    String name
    int age

    def static user(@DelegatesTo(Conf.class) Closure<Conf> closure) {
        Conf conf = new Conf()
        // 容许对象(conf)调用闭包
        DefaultGroovyMethods.with(conf, closure)
        println(conf.name)
        println(conf.age)
    }

    static void main(String[] args) {
        user {
            withName "test"
            withAge 12 }
    }
}

2.4 Singletone

不需要自己在实现单例模式,直接用此注解即可实现,还可以配置是否要启用懒加载。

@Singleton(lazy = true)
class Util {
    def count(text) {
        text.size()
    }
}

println Util.instance.count("Hello World!")

三、加载器

3.1 GroovyClassLoader

GroovyClassLoader可以通过parseClass()解析文件,通过此方式将在进程缓存中缓存编译的class字节码信息,这样重新解析相同文件就不会反复执行。
另外一个需要注意的是parseClass()比较适合解析脚本,如果是嵌套类或者接口就不适合用此方式解析,因为只会返回第一个类或者接口。原因
另外用此方式收集嵌套接口或者类信息,会导致脚本执行import模块对象可能会报missing property的相关错误。从表面看是缺少模块属性,实际是由于parseClass()动态解析并获取第一个内部类导致的。
如果用IDE执行就不会报错,因为IDE会使用-cp引用编译后的class文件,不会有动态加载class找不到import相关class的情况。在linux系统下可以用下面的命令方式编译groovy文件,当然如果存在某些文件是配置文件,那就需要手动拷贝文件到具体存放编译文件的路径下,此处的编译文件存放路径是$WORKSPACE/out。

groovyc -cp . -d $WORKSPACE/out `find . -name "*.groovy"`

四、DSL

TODO

五、个人遇到问题的FAQ

4.1 groovy命令行执行groovy脚本提示Caught: java.lang.ClassNotFoundException

对一个对象实例做深度拷贝操作,在jetbrains中执行是没问题的,但是用命令行方式进行就报错了。导致这个问题的原因是用groovy命令行方式执行没有编译过程,所以classLoader找不到相关的class类。

import groovy.transform.builder.Builder
import groovy.transform.builder.SimpleStrategy

@Builder(builderStrategy = SimpleStrategy)
class Copy implements Serializable{
    String name
    Map<String, String> origins

    static void main(String[] args) {
        Copy copy = new Copy().setName("123")
        ByteArrayOutputStream bos = new ByteArrayOutputStream()
        ObjectOutputStream oos = new ObjectOutputStream(bos)
        oos.writeObject(copy)
        oos.close()

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray())
        ObjectInputStream ois = new ObjectInputStream(bis)
        Copy copy2 = ois.readObject()
        println copy
        println copy2
        println System.identityHashCode(copy.getName())
        println System.identityHashCode(copy2.getName())
        println copy.getName()
        println copy2.getName()
    }
}

解决此问题的一个办法是用groovyc编译生成class类,然后用java copy.class的方式执行。还有一个办法是先把要调用到的类编译出来,然后要执行groovy脚本的时候使用:groovy -cp your_classes_lib your_script.groovy的模式来执行。

4.2 Intellij执行groovy脚本提示错误: 找不到或无法加载主类 org.codehaus.groovy.tools.GroovyStarter

在intellij中执行grooxy脚本的报错信息如下所示: 如果groovy在安装和intellij项目工程配置合理的情况下,可能是由于未加载groovy-xxx.jar包下无法找到GroovyStarter类,可以尝试修改配置启动项,在VM options中添加-Dfile.encoding=UTF-8 -classpath D:\xxx\lib\groovy-4.0.2.jar;

六、参考文献