Groovy简介与使用

简介

Groovy是构建在JVM上的一个轻量级却强大的动态语言, 它结合了Python、Ruby和Smalltalk的许多强大的特性.

Groovy就是用Java写的 , Groovy语法与Java语法类似, Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码, 相对于Java, 它在编写代码的灵活性上有非常明显的提升,Groovy 可以使用其他 Java 语言编写的库.

使用

下载SDK

  • Groovy Console
  • 安装IDEA groovy插件

应用

ElasticSearch, Jenkins 都支持执行Groovy脚本
项目构建工具Gradle就是Groovy实现的


Groovy语法特性(相比于Java)

  1. 不需要分号

  2. return关键字可省略, 方法的最后一句表达式可作为返回值返回 (视具体情况使用, 避免降低可读性)

  3. 类的默认作用域是public, 不需要getter/setter方法

  4. def关键字定义的变量类型都是Object, 任何变量, 方法都能用def定义/声明 , 在 Groovy 中 “一切都是对象 "

  5. 导航操作符 ( ?. )可帮助实现对象引用不为空时方法才会被调用

    // java
    if (object != null) {
        object.getFieldA();
    }
    // groovy
    object?.getFieldA()
    
  6. 命令链, Groovy 可以使你省略顶级语句方法调用中参数外面的括号。“命令链”功能则将这种特性继续扩展,它可以将不需要括号的方法调用串接成链,既不需要参数周围的括号,链接的调用之间也不需要点号

    def methodA(String name) {
        println("A: " + name)
        return this
    }
    def methodB(String name) {
        println("B: " + name)
        return this
    }
    def methodC() {
        println("C")
        return this
    }
    def methodD(String name) {
        println("D: " + name)
        return this
    }
    
    methodA("xiaoming")
    methodB("zhangsan")
    methodC()
    methodD("lisi")
    
    // 不带参数的链中需要用括号 
    methodA "xiaoming" methodB "zhangsan" methodC() methodD "lisi"
    
  1. 闭包. 闭包是一个短的匿名代码块。每个闭包会被编译成继承groovy.lang.Closure类的类,这个类有一个叫call方法,通过该方法可以传递参数并调用这个闭包.

    def hello = {println "Hello World"}
    hello.call()
    
    // 包含形式参数
    def hi = {
        person1, person2 -> println "hi " + person1 + ", "+ person2
    }
    hi.call("xiaoming", "xiaoli")
    
    // 隐式单个参数, 'it'是Groovy中的关键字
    def hh = {
        println("haha, " + it)
    }
    hh.call("zhangsan")
    
  1. with语法, (闭包实现)

    // Java
    public class JavaDeamo {
        public static void main(String[] args) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(Calendar.MONTH, Calendar.DECEMBER);
            calendar.set(Calendar.DATE, 4);
            calendar.set(Calendar.YEAR, 2018);
            Date time = calendar.getTime();
            System.out.println(time);
        }
    }
    // Groovy
    Calendar calendar = Calendar.getInstance()
    calendar.with {
        // it 指 calendar 这个引用
        it.set(Calendar.MONTH, Calendar.DECEMBER)
        // 可以省略it, 使用命令链
        set Calendar.DATE, 4
        set Calendar.YEAR, 2018
        // calendar.getTime()
        println(getTime())
        // 省略get, 对于get开头的方法名并且
        println(time)
    }
    
  1. 数据结构的原生语法, 写法更便捷

    def list = [11, 12, 13, 14] // 列表, 默认是ArrayList
    def list = ['Angular', 'Groovy', 'Java'] as List // 字符串列表
    // 同list.add(8)
    list << 8
    
    [1, 2, [3, 4], 5] // 嵌套列表
    ['Groovy', 21, 2.11] // 异构的对象引用列表
    [] // 一个空列表
    
    def set = ["22", "11", "22"] as Set // LinkedHashSet, as运算符转换类型
    
    def map = ['TopicName': 'Lists', 'TopicName': 'Maps'] // map, LinkedHashMap
    [:] // 空map
    
    // 循环
    map.each {
        print it.key
    }
    
  1. Groovy Truth

所有类型都能转成布尔值,比如null, void 对象, 等同于 0 或空的值,都会解析为false,其他则相当于true

  1. groovy支持DSL(Domain Specific Languages领域特定语言), DSL旨在简化以Groovy编写的代码,使得它对于普通用户变得容易理解

    借助命令链编写DSL

    // groovy代码
    show = { println it }
    square_root = { Math.sqrt(it) }
    
    def please(action) {
      [the: { what ->
        [of: { n -> action(what(n)) }]
      }]
    }
    
    // DSL 语言: please show the square_root of 100  (请显示100的平方根)
    
    // 调用, 等同于:please(show).the(square_root).of(100)
    please show the square_root of 100
    // ==> 10.0
    
  1. Java 的 == 实际相当于 Groovy 的 is() 方法,而 Groovy 的 == 则是一个更巧妙的 equals()。 在Groovy中要想比较对象的引用,不能用 ==,而应该用 a.is(b)


Groovy与Java项目集成使用

项目中引入groovy依赖

            <dependency>
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy-all</artifactId>
                <version>x.y.z</version>
            </dependency>

常见的集成机制:

GroovyShell

GroovyClassLoader

GroovyScriptEngine

JSR 223 javax.script API

GroovyShell

GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果

解析为脚本(groovy.lang.Script)运行

        GroovyShell groovyShell = new GroovyShell();
        groovyShell.evaluate("println \"hello world\"");

GroovyClassLoader

用 Groovy 的 GroovyClassLoader ,动态地加载一个脚本并执行它的行为。GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。

GroovyClassLoader loader = new GroovyClassLoader();
Class groovyClass = loader.parseClass(new File(groovyFileName)); // 也可以解析字符串
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod("run", "helloworld");

GroovyScriptEngine

groovy.util.GroovyScriptEngine 类为 GroovyClassLoader 其上再增添一个能够处理脚本依赖及重新加载的功能层, GroovyScriptEngine可以从指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本

你可以使用一个CLASSPATH集合(url或者路径名称)初始化GroovyScriptEngine,之后便可以让它根据要求去执行这些路径中的Groovy脚本了.GroovyScriptEngine同样可以跟踪相互依赖的脚本,如果其中一个被依赖的脚本发生变更,则整个脚本树都会被重新编译和加载。

        GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine(file.getAbsolutePath());
        groovyScriptEngine.run("hello.groovy", new Binding())

JSR-223

JSR-223 是 Java 中标准的脚本框架调用 API。从 Java 6 开始引入进来,主要目用来提供一种常用框架,以便从 Java 中调用多种语言

ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
// 编译成类
groovyEngine.compile(script)
// 直接执行
groovyEngine.eval(script)


Groovy实现相关原理

groovy负责词法、语法解析groovy文件,然后用ASM生成普通的java字节码文件,供jvm使用。

Groovy代码文件与class文件的对应关系

作为基于JVM的语言,Groovy可以非常容易的和Java进行互操作,但也需要编译成class文件后才能运行,所以了解Groovy代码文件和class文件的对应关系,有助于更好地理解Groovy的运行方式和结构。

对于没有任何类定义

如果Groovy脚本文件里只有执行代码,没有定义任何类(class),则编译器会生成一个Script的子类,类名和脚本文件的文件名一样,而脚本的代码会被包含在一个名为run的方法中,同时还会生成一个main方法,作为整个脚本的入口。

对于仅有一个类

如果Groovy脚本文件里仅含有一个类,而这个类的名字又和脚本文件的名字一致,这种情况下就和Java是一样的,即生成与所定义的类一致的class文件, Groovy类都会实现groovy.lang.GroovyObject接口。

对于多个类

如果Groovy脚本文件含有一个或多个类,groovy编译器会很乐意地为每个类生成一个对应的class文件。如果想直接执行这个脚本,则脚本里的第一个类必须有一个static的main方法。

对于有定义类的脚本

如果Groovy脚本文件有执行代码, 并且有定义类, 那么所定义的类会生成对应的class文件, 同时, 脚本本身也会被编译成一个Script的子类,类名和脚本文件的文件名一样


Spring对Groovy以及动态语言的支持

Spring 从2.0开始支持将动态语言集成到基于 Spring 的应用程序中。Spring 开箱即用地支持 Groovy、JRuby 和 BeanShell。以 Groovy、JRuby 或任何受支持的语言编写的应用程序部分可以无缝地集成到 Spring 应用程序中。应用程序其他部分的代码不需要知道或关心单个 Spring bean 的实现语言。

动态语言支持将 Spring 从一个以 Java 为中心的应用程序框架改变成一个以 JVM 为中心的应用程序框架

Spring 通过 ScriptFactory 和 ScriptSource 接口支持动态语言集成。ScriptFactory 接口定义用于创建和配置脚本 Spring bean 的机制。理论上,所有在 JVM 上运行语言都受支持,因此可以选择特定的语言来创建自己的实现。ScriptSource 定义 Spring 如何访问实际的脚本源代码;例如,通过文件系统, URL, 数据库。

在使用基于 Groovy 的 bean 时,则有几种选择:

  • 将 Groovy 类编译成普通的 Java 类文件

  • 在一个 .groovy 文件中定义 Groovy 类或脚本

  • 在 Spring 配置文件中以内联方式编写 Groovy 脚本

  1. 配置编译的 Groovy 类, 和Java一样的用法, 定义groovy class, 使用<bean/>创建bean
class Test {
    def printDate() {
        println(new Date());
    }
}
    <bean id="test" class="com.qj.study.groovytest.spring.Test" />
ClassPathXmlApplicationContext context = newClassPathXmlApplicationContext("applicationContext.xml");
Test bean = (Test) context.getBean("test");
bean.printDate();
  1. 配置来自 Groovy 脚本的 bean

    • <bean/>

    • <lang:groovy>

  • <bean/>示例:
 <bean id="demo" class="org.springframework.scripting.groovy.GroovyScriptFactory">
        <constructor-arg value="classpath:script/ScriptBean.groovy"/>
 </bean>
 <bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>
  • <lang:groovy/>示例:
    <lang:groovy id="demo" script-source="classpath:script/ScriptBean.groovy">
    </lang:groovy>
    <bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>

实现过程:

Groovy 语言集成通过 ScriptFactory 的 GroovyScriptFactory 实现得到支持

当 Spring 装载应用程序上下文时,它首先创建工厂 bean(这里是GroovyScriptFactory 类型的bean)。然后,执行 ScriptFactoryPostProcessor bean中的postProcessBeforeInstantiation方法,用实际的脚本对象替换所有的工厂 bean。

ScriptFactoryPostProcessor:

    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
        // 只处理ScriptFactory类型的bean
        if (!ScriptFactory.class.isAssignableFrom(beanClass)) {
            return null;
        }
        // ...
        // 加载并解析groovy代码, 在scriptBeanFactory中注册BeanDefinition
        prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName);
        // ...
    }


     // prepareScriptBeans调用createScriptedObjectBeanDefinition
    protected BeanDefinition createScriptedObjectBeanDefinition(BeanDefinition bd, String scriptFactoryBeanName,
            ScriptSource scriptSource, @Nullable Class<?>[] interfaces) {

        GenericBeanDefinition objectBd = new GenericBeanDefinition(bd);
        objectBd.setFactoryBeanName(scriptFactoryBeanName);
        // 指定工厂方法, ScriptFactory.getScriptedObject, 创建脚本的Java对象 
        objectBd.setFactoryMethodName("getScriptedObject");
        objectBd.getConstructorArgumentValues().clear();
        objectBd.getConstructorArgumentValues().addIndexedArgumentValue(0, scriptSource);
        objectBd.getConstructorArgumentValues().addIndexedArgumentValue(1, interfaces);
        return objectBd;
    }

创建bean的时候, SimpleInstantiationStrategy.instantiate

                 // 调用工厂方法创建beanInstance
                Object result = factoryMethod.invoke(factoryBean, args);
                if (result == null) {
                    result = new NullBean();
                }

GroovyScriptFactory.getScriptedObject

                      // 通过groovyClassLoader 加载并解析类
                    this.scriptClass = getGroovyClassLoader().parseClass(                           scriptSource.getScriptAsString(), scriptSource.suggestedClassName());

                    if (Script.class.isAssignableFrom(this.scriptClass)) {
                          // 如果是groovy 脚本, 那么运行脚本, 将结果的类作为Bean的类型
                        Object result = executeScript(scriptSource, this.scriptClass);
                        this.scriptResultClass = (result != null ? result.getClass() : null);
                        return result;
                    }
                    else {
                          // 不是脚本, 直接返回类
                        this.scriptResultClass = this.scriptClass;
                    }
    protected Object executeScript(ScriptSource scriptSource, Class<?> scriptClass) throws ScriptCompilationException {
        try {
            GroovyObject goo = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance();

            // GroovyObjectCustomizer 是一个回调,Spring 在创建一个 Groovy bean 之后会调用它。可以对一个 Groovy bean 应用附加的逻辑,或者执行元编程
            if (this.groovyObjectCustomizer != null) {
                this.groovyObjectCustomizer.customize(goo);
            }

            if (goo instanceof Script) {
                // A Groovy script, probably creating an instance: let's execute it.
                return ((Script) goo).run();
            }
            else {
                // An instance of the scripted class: let's return it as-is.
                return goo;
            }
        }
        catch (NoSuchMethodException ex) {
            // ...
    }

最终在ScriptFactoryPostProcessor中, scriptBeanFactory保存了所有通过脚本创建的bean, scriptSourceCache缓存了所有的脚本信息

    final DefaultListableBeanFactory scriptBeanFactory = new DefaultListableBeanFactory();

    /** Map from bean name String to ScriptSource object */
    private final Map<String, ScriptSource> scriptSourceCache = new HashMap<String, ScriptSource>();
  • refresh参数
<lang:groovy id="refresh"  refresh-check-delay="1000"
                 script-source="classpath:script/RefreshBean.groovy">
    </lang:groovy>

创建的是JdkDynamicAopProxy代理对象, 在每一次调用这个代理对象的方法的时候, 都回去校验被代理对象是否需要刷新, 通过比对脚本文件的最后更新时间和设定的更新时间间隔, 如果需要刷新则重新加载这个groovy文件, 并编译, 然后创建一个新的bean并注册进行替换

3.内联方式配置

inline script标签, 从配置中读取源代码

   <lang:groovy id="inline">
        <lang:inline-script> 
            <![CDATA[
            class InlineClass {
                // xxxxx ...
            }
            ]]>
        </lang:inline-script>
    </lang:groovy>

综上, 扩展一下, 脱离xml配置, 可以从数据库中定时加载groovy代码, 构建/更新/删除BeanDefinition


Groovy运行沙盒

沙盒原理也叫沙箱,英文sandbox。在计算机领域指一种虚拟技术,且多用于计算机安全技术。安全软件可以先让它在沙盒中运行,如果含有恶意行为,则禁止程序的进一步运行,而这不会对系统造成任何危害。

举个例子:

docker容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源。不同的容器之间相互隔离。CGroup实现资源控制, Namespace实现访问隔离, rootfs实现文件系统隔离。


对于嵌入Groovy的Java系统, 如果暴露接口, 可能存在的隐患有

  • 通过Java的Runtime.getRuntime().exec()方法执行shell, 操作服务器.....

  • 执行System.exit(0)

  • dump 内存中的Class, 修改内存中的缓存数据

ElasticSearch Groovy 脚本 远程代码执行漏洞


Groovy提供了编译自定义器(Compilation customizers), 无论你使用 groovyc 还是采用 GroovyShell 来编译类,要想执行脚本,实际上都会使用到编译器配置compiler configuration)信息。这种配置信息保存了源编码或类路径这样的信息,而且还用于执行更多的操作,比如默认添加导入,显式使用 AST(语法树) 转换,或者禁止全局 AST 转换, 编译自定义器的目标在于使这些常见任务易于实现。CompilerConfiguration 类就是切入点。


groovy sandbox的实现 -> https://github.com/jenkinsci/groovy-sandbox

实现过程:

groovy-sandbox实现了一个SandboxTransformer, 扩展自CompilationCustomizer, 在Groovy代码编译时进行转换. 脚本转换后, 让脚本执行的每一步都会被拦截, 调用Checker进行检查

可拦截所有内容,包括

  • 方法调用(实例方法和静态方法)
  • 对象分配(即除了“this(...)”和“super(...)”之外的构造函数调用
  • 属性访问(例如,z = foo.bar,z = foo。“bar”)和赋值(例如,foo.bar = z,foo。“bar”= z)
  • 数组访问和赋值

当然, 执行性能也会受到一些的影响

示例: Jenkins Pipline支持在Groovy沙盒中执行Groovy脚本


image.png


其他:

Groovy元编程 原文 译文

Groovy的ClassLoader体系