目录

沈超琦的个人博客

Stay Foolish,Stay Hungry

X

Groovy脚本语言分享

1 Groovy与JAVA的区别

Groovy语言一直在努力亲近Java开发人员。在设计Groovy语言的时候,我们遵循最小标新立异原则,努力让那些Java开发背景的开发者容易上手并学会。下面我们列举Groovy和Java的一些主要区别。

1 默认导入

下面的包和类是默认导入的,也就是说不必精确使用 import 语句来导入它们:

  • java.io.*
  • java.lang.*
  • java.math.BigDecimal
  • java.math.BigInteger
  • java.net.*
  • java.util.*
  • groovy.lang.*
  • groovy.util.*

2 动态方法(Multi-methods)

在Groovy里,方法的调用是在运行时动态决定。这一特性叫做运行时分发(runtime dispatch)或动态方法(multi-methods)。也就是说方法的最后调用是根据传入参数在运行时的类型所决定。在Java里,这一点是不一样的:在编译时就决定了方法的参数类型。

下面的代码,我们采用Java风格,在Groovy和Java都可以编译通过,但是运行结果不一样:

int method(String arg) {
    return 1;
}
int method(Object arg) {
    return 2;
}
Object o = "Object";
int result = method(o);

在Java里,结果是

assertEquals(2, result);

但是Groovy里,结果是

assertEquals(1, result);

原因是Java使用的是静态声明的类型信息,这里o被声明为Object,但是Groovy是在运行时决定,当方法最终被调用时,因为这里o实际是一个字符串,因此最终String版本的方法被调用。

3 数组初始化

在Groovy,{…}已经被用作闭包,也就是说你不能使用下面的语法创建数组

int[] array = { 1, 2, 3}

你应该这样声明并初始化一个数组

int[] array = [1,2,3]

4 包范围可见性(Package scope visibility)

在Groovy里,省略字段的修饰符不会像Java一样使其成为包私有属性(package-private field)

class Person {
    String name
}

这里,我们创建了一个属性,它是私有的,而且自动关联了getter和setter方法。如果我们要创建一个包私有属性,可以添加@PackageScope注解来实现:

class Person {
    @PackageScope String name
}

5 ARM块

ARM(Automatic Resource Management 自动资源管理)块从Java7开始支持,但是Groovy不支持。相应地,Groovy依赖于闭包来实现类似的功能。示例:

Path file = Paths.get("/path/to/file");
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

} catch (IOException e) {
    e.printStackTrace();
}

在Groovy里可以写成:

new File('/path/to/file').eachLine('UTF-8') {
   println it
}

或者也可以写成跟Java类似风格:

new File('/path/to/file').withReader('UTF-8') { reader ->
   reader.eachLine {
       println it
   }
}

6 内部类

Groovy遵循了Java的匿名内部类以及嵌套内的特点。但是它并没有完全依照Java语言规范,因此在使用前应该记住它们是有区别的。Groovy的实现和groovy.lang.Clouser类的风格有些类似,但也有不同点。比如在访问私有字段和方法以及局部变量没有final等。

6.1 静态内部类

这是一个静态内部类的例子:

class A {
    static class B {}
}

new A.B()

6.2 匿名内部类

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

CountDownLatch called = new CountDownLatch(1)

Timer timer = new Timer()
timer.schedule(new TimerTask() {
    void run() {
        called.countDown()
    }
}, 0)

assert called.await(10, TimeUnit.SECONDS)

6.3 创建非静态内部类实例

在Java里,你可以这样写:

public class Y {
    public class X {}
    public X foo() {
        return new X();
    }
    public static X createX(Y y) {
        return y.new X();
    }
}

Groovy不支持y.new X()语法,但你可以写成new X(y),像下面的代码:

public class Y {
    public class X {}
    public X foo() {
        return new X()
    }
    public static X createX(Y y) {
        return new X(y)
    }
}

7 lambda表达式

Java 8 支持lambda表达式和方法引用

Runnable run = () -> System.out.println("Run");
list.forEach(System.out::println);

Java8的lambda表达式或多或少被认为是匿名内部类。Groovy不支持这样的语法,但是可以使用闭包代替:

Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)

8 GStrings

使用双引号修饰的字符串被解释为GString值。如果一个字符串里含有美元符号在Groovy和Java的编译器里将会产生编译错误。

当然,Groovy会自动在GString和String之间进行类型转换,就像Java可以接受一个Object参数然后检查其实际类型一样。

9 字符串和字符

在Groovy里,使用单引号修饰的被当成String类型,使用双引号修饰的可以当成GString类型或String类型。取决于字面常量。

assert 'c'.getClass()==String
assert "c".getClass()==String
assert "c${1}".getClass() in GString

如果声明是char类型,Groovy会自动将单个字符从String类型转换为char类型。如果被调用的方法声明的参数类型是char,我们需要强制类型转换为char类型。

char a='a'
assert Character.digit(a, 16)==10 : 'But Groovy does boxing'
assert Character.digit((char) 'a', 16)==10

try {
  assert Character.digit('a', 16)==10
  assert false: 'Need explicit cast'
} catch(MissingMethodException e) {
}

Groovy支持两种风格的类型转换,在转换成char类型的时候,当个字符和多个字符转换有些不一样。对于多个字符转换成char类型,Groovy会选择第一个字符,这一点不像C语言,会直接失败。

// for single char strings, both are the same
assert ((char) "c").class==Character
assert ("c" as char).class==Character

// for multi char strings they are not
try {
  ((char) 'cx') == 'c'
  assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

10 ==的行为

在Java里,==意味着基本类型相等或对象类型相等。在Groovy里,==会转换成a.compareTo(b)==0,如果他们是Comparable,就是使用a.equals(b),否则检查基本类型,也就是is,比如a.is(b)

11 不同的关键字

Groovy比Java有更多的关键字,请不要把它们当变量名使用

  • in
  • trait

2 Groovy的特性对JAVA开发的帮助

2.1 运行动态脚本

在springboot项目中,开发人员可以很方便的完成各种功能的开发和封装,提供流行的restful api接口。然而对项目功能的测试,大部分情况会通过预先编写测试用例进行,甚至开发人员会开发测试专用的restful api接口来完成基本功能的测试,这无疑增加了开发成本,且会产生很多"用过即废"的代码。

**那么能否实现动态组合或运行spring容器中注册的各个bean的功能、动态运行bean的方法呢?在项目中集成groovy动态脚本的能力即可**。

** 集成groovy的好处:**

  1. groovy跟java都是基于jvm的语言,可以在java项目中集成groovy并充分利用groovy的动态功能;
  2. groovy兼容几乎所有的java语法,开发者完全可以将groovy当做java来开发,甚至可以不使用groovy的特有语法,仅仅通过引入groovy并使用它的动态能力;
  3. groovy可以直接调用项目中现有的java类(通过import导入),通过构造函数构造对象并直接调用其方法并返回结果;
  4. groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。因此我们可以通过将spring的bean预设到GroovyShell运行环境中,在groovy动态脚本中直接调用spring容器中bean来调用其方法,这点对于spring项目非常方便!(我猜测odin的groovy组件就是通过我下面demo类似的方式的原理)

2.1.1 演示

2.1.1.1直接调用java类

package com.linkjb.fastidious.controller;

/**
 * @ClassName TestService
 * @Description TODO
 * @Author shark
 * @Data 2020/11/13 18:02
 **/
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.springframework.stereotype.Service;

@Service
public class TestService {

    public String testQuery(long id){
        return "Test query success, id is " + id;
    }

    public static void main(String[] args) {
        Binding groovyBinding = new Binding();
        groovyBinding.setVariable("testService", new TestService());
        GroovyShell groovyShell = new GroovyShell(groovyBinding);

        String scriptContent = "import com.linkjb.fastidious.controller.TestService\n" +
                "def query = new TestService().testQuery(1L);\n" +
                "query";

        /*String scriptContent = "def query = testService.testQuery(2L);\n" +
                "query";*/

        Script script = groovyShell.parse(scriptContent);
        System.out.println(script.run());
    }
}

返回结果:

Test query success, id is 1

这种方式在groovy动态脚本中将类import后直接new了一个新对象,并调用对象的方法。

2.1.1.2 通过GroovyShell预设对象

groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。

        Binding groovyBinding = new Binding();
        groovyBinding.setVariable("testService", new TestService());
        GroovyShell groovyShell = new GroovyShell(groovyBinding);

        String scriptContent = "def query = testService.testQuery(2L);\n" +
                "query";

        Script script = groovyShell.parse(scriptContent);
        System.out.println(script.run());

返回结果:

Test query success, id is 2

这种方式在groovy动态脚本中将类import后直接new了一个新对象,并调用对象的方法

这种方式通过Binding对象的setVariable方法设置了预设对象testService,在动态脚本中便可以直接调用testService的方法。简单看下Binding类setVariable方法的源码

public void setVariable(String name, Object value) {
        if (this.variables == null) {
            this.variables = new LinkedHashMap();
        }

        this.variables.put(name, value);
    }

实际上,Binding对象维护了一个Map类型的属性variables,通过setVariable方法将预设对象和预设对象名称存储到了variables属性中,动态运行时会尝试道variables中获取对应名称的对象,如果存在再尝试调用其方法。

2.1.1.3 groovy脚本中调用springbean的方法

到这里已经很清晰了,我们只要能获取spring容器中所有的bean,通过Binding的setVariable将spring所有的bean预设进GroovyShell运行环境,在动态脚本中便可以直接调用bean的方法。这种我们对spring项目中的service层、controller层、DAO层等注册的bean均可以通过这种方式实现动态调用。

2.1.1.3.1 springboot接口动态运行groovy脚本
下面以一个springboot接口动态运行groovy脚本的示例工程为例,讲述如何在springboot接口中动态运行groovy脚本。
2.1.1.3.1.1 引入groovy-all依赖
<dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>2.4.7</version>
        </dependency>
2.1.1.3.1.2 Service层示例类
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.springframework.stereotype.Service;

@Service
public class TestService {
    public String testQuery(long id){
        return "Test query success, id is " + id;
    }

TestService 类实现了一个简单的testQuery方法,springboot通过扫描到@Service注解会将生成TestService的bean并注册到应用上下文中,beanName为"testService".

2.1.1.3.1.3 springboot的Configuration类中设置Binding

首先配置类可以实现org.springframework.context.ApplicationContextAware接口用来获取应用上下文,然后再配置类中通过应用上下文获取所有的bean并注册到groovy的Binding中

package com.linkjb.fastidious.config;

/**
 * @ClassName GroovyBindingConfig
 * @Description TODO
 * @Author shark
 * @Data 2020/11/13 17:57
 **/
import groovy.lang.Binding;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@Configuration
public class GroovyBindingConfig implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    //@Bean("groovyBinding")
    public Binding groovyBinding() {
        Binding groovyBinding = new Binding();

        Map<String, Object> beanMap = applicationContext.getBeansOfType(Object.class);
        //遍历设置所有bean,可以根据需求在循环中对bean做过滤
        for (String beanName : beanMap.keySet()) {
            groovyBinding.setVariable(beanName, beanMap.get(beanName));
        }
        return groovyBinding;
    }

    @Bean("groovyBinding1")
    public Binding groovyBinding1() {
        Map<String, Object> beanMap = applicationContext.getBeansOfType(Object.class);
        return new Binding(beanMap); //如果不需要对bean做过滤,直接用beanMap构造Binding对象即可
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;

    }
}

如果不需要对bean做过滤,可以通过注释掉的方法直接从应用上下文中获取beanMap并直接构造Binding的variables中;当然上面示例applicationContext.getBeansOfType方法也可以指定获取bean的类型.

需要注意的是:上面这种方法注册的到binding中beanMap是不包含groovyBinding这个对象本身的(先后顺序的原因),如果需要将binding对象本身(也是一个bean)注册,也很简单,只需要将Binding的bean生成放在GroovyBindingConfig之前,并且在实现ApplicationContextAware接口的setApplicationContext方法中进行variables的设置即可,但建议不这样做,因为这样就可以通过脚本对Binding对象本身造成破坏,不太优雅~

2.1.1.3.1.4实现用于groovy动态脚本运行的controller
package com.linkjb.fastidious.controller;

import com.linkjb.fastidious.entity.vo.CountData;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * @ClassName GroovyController
 * @Description TODO
 * @Author shark
 * @Data 2020/11/13 17:15
 **/
@RestController
@RequestMapping("/groovy")
public class GroovyController {
    @Autowired
    private Binding groovyBinding;

    private GroovyShell groovyShell;



    @PostConstruct
    public void init(){
        GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader());
        CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
        compilerConfiguration.setSourceEncoding("utf-8");
        compilerConfiguration.setScriptBaseClass(Script.class.getName());

        groovyShell = new GroovyShell(groovyClassLoader, groovyBinding, compilerConfiguration);
    }

    @RequestMapping(value = "/execute", method = RequestMethod.POST)
    public String execute(@RequestBody String scriptContent) {
        Script script = groovyShell.parse(scriptContent); //此处简单示例,实际应用中可根据脚本特征将script存储, 下次运行时可根据脚本特征直接获取Script对象,避免parse的成本
        return String.valueOf(script.run());
    }



}

将binding对象注入后,在初始化方法init()中用binding对象构造GroovyShell对象,在提供的execute接口实现中用GroovyShell对象生成Script脚本对象,并调用Script的run()方法运行动态脚本并返回结果。

上述示例中只是一个简单实现,在接口方法execute中,每次脚本运行前都会通过groovyShell来parse出一个Script 对象,这其实是有成本的,实际应用中可根据脚本特征(如md5值等)将script存储, 下次运行时可根据脚本特征直接获取Script对象,避免parse的成本。

2.1.1..3.1.5 实现用于groovy动态脚本运行的controller

上述接口定义了一个post方法,path:/groovy/execute,运行后直接用postman调用测试testService的方法,结果如下:

image.png

显然,通过接口直接用groovy脚本调用了testService这个bean的方法,非常简单。

2.1.1.3.1.6 注意事项

在实际项目中,特别是生产环境,虽然可以方便的调用应用中的bean或者类的方法,但随意调用也可能引发不可避免的灾难,所以对运行groovy动态脚本的接口要注意做好严格的权限控制!!!

2.1.1.2 规则引擎的应用

直接看项目


标题:Groovy脚本语言分享
作者:sharkshen@outlook.com
地址:https://www.linkjb.com/articles/2020/11/15/1605442029796.html