Groovy簡(jiǎn)介與使用

簡(jiǎn)介

Groovy是構(gòu)建在JVM上的一個(gè)輕量級(jí)卻強(qiáng)大的動(dòng)態(tài)語(yǔ)言, 它結(jié)合了Python、Ruby和Smalltalk的許多強(qiáng)大的特性.

Groovy就是用Java寫(xiě)的 , Groovy語(yǔ)法與Java語(yǔ)法類似, Groovy 代碼能夠與 Java 代碼很好地結(jié)合,也能用于擴(kuò)展現(xiàn)有代碼, 相對(duì)于Java, 它在編寫(xiě)代碼的靈活性上有非常明顯的提升,Groovy 可以使用其他 Java 語(yǔ)言編寫(xiě)的庫(kù).

使用

下載SDK

  • Groovy Console
  • 安裝IDEA groovy插件

應(yīng)用

ElasticSearch, Jenkins 都支持執(zhí)行Groovy腳本
項(xiàng)目構(gòu)建工具Gradle就是Groovy實(shí)現(xiàn)的


Groovy語(yǔ)法特性(相比于Java)

  1. 不需要分號(hào)

  2. return關(guān)鍵字可省略, 方法的最后一句表達(dá)式可作為返回值返回 (視具體情況使用, 避免降低可讀性)

  3. 類的默認(rèn)作用域是public, 不需要getter/setter方法

  4. def關(guān)鍵字定義的變量類型都是Object, 任何變量, 方法都能用def定義/聲明 , 在 Groovy 中 “一切都是對(duì)象 "

  5. 導(dǎo)航操作符 ( ?. )可幫助實(shí)現(xiàn)對(duì)象引用不為空時(shí)方法才會(huì)被調(diào)用

    // java
    if (object != null) {
        object.getFieldA();
    }
    // groovy
    object?.getFieldA()
    
  6. 命令鏈, Groovy 可以使你省略頂級(jí)語(yǔ)句方法調(diào)用中參數(shù)外面的括號(hào)?!懊铈湣惫δ軇t將這種特性繼續(xù)擴(kuò)展,它可以將不需要括號(hào)的方法調(diào)用串接成鏈,既不需要參數(shù)周圍的括號(hào),鏈接的調(diào)用之間也不需要點(diǎn)號(hào)

    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")
    
    // 不帶參數(shù)的鏈中需要用括號(hào) 
    methodA "xiaoming" methodB "zhangsan" methodC() methodD "lisi"
    
  1. 閉包. 閉包是一個(gè)短的匿名代碼塊。每個(gè)閉包會(huì)被編譯成繼承g(shù)roovy.lang.Closure類的類,這個(gè)類有一個(gè)叫call方法,通過(guò)該方法可以傳遞參數(shù)并調(diào)用這個(gè)閉包.

    def hello = {println "Hello World"}
    hello.call()
    
    // 包含形式參數(shù)
    def hi = {
        person1, person2 -> println "hi " + person1 + ", "+ person2
    }
    hi.call("xiaoming", "xiaoli")
    
    // 隱式單個(gè)參數(shù), 'it'是Groovy中的關(guān)鍵字
    def hh = {
        println("haha, " + it)
    }
    hh.call("zhangsan")
    
  1. with語(yǔ)法, (閉包實(shí)現(xiàn))

    // 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 這個(gè)引用
        it.set(Calendar.MONTH, Calendar.DECEMBER)
        // 可以省略it, 使用命令鏈
        set Calendar.DATE, 4
        set Calendar.YEAR, 2018
        // calendar.getTime()
        println(getTime())
        // 省略get, 對(duì)于get開(kāi)頭的方法名并且
        println(time)
    }
    
  1. 數(shù)據(jù)結(jié)構(gòu)的原生語(yǔ)法, 寫(xiě)法更便捷

    def list = [11, 12, 13, 14] // 列表, 默認(rèn)是ArrayList
    def list = ['Angular', 'Groovy', 'Java'] as List // 字符串列表
    // 同list.add(8)
    list << 8
    
    [1, 2, [3, 4], 5] // 嵌套列表
    ['Groovy', 21, 2.11] // 異構(gòu)的對(duì)象引用列表
    [] // 一個(gè)空列表
    
    def set = ["22", "11", "22"] as Set // LinkedHashSet, as運(yùn)算符轉(zhuǎn)換類型
    
    def map = ['TopicName': 'Lists', 'TopicName': 'Maps'] // map, LinkedHashMap
    [:] // 空map
    
    // 循環(huán)
    map.each {
        print it.key
    }
    
  1. Groovy Truth

所有類型都能轉(zhuǎn)成布爾值,比如null, void 對(duì)象, 等同于 0 或空的值,都會(huì)解析為false,其他則相當(dāng)于true

  1. groovy支持DSL(Domain Specific Languages領(lǐng)域特定語(yǔ)言), DSL旨在簡(jiǎn)化以Groovy編寫(xiě)的代碼,使得它對(duì)于普通用戶變得容易理解

    借助命令鏈編寫(xiě)DSL

    // groovy代碼
    show = { println it }
    square_root = { Math.sqrt(it) }
    
    def please(action) {
      [the: { what ->
        [of: { n -> action(what(n)) }]
      }]
    }
    
    // DSL 語(yǔ)言: please show the square_root of 100  (請(qǐng)顯示100的平方根)
    
    // 調(diào)用, 等同于:please(show).the(square_root).of(100)
    please show the square_root of 100
    // ==> 10.0
    
  1. Java 的 == 實(shí)際相當(dāng)于 Groovy 的 is() 方法,而 Groovy 的 == 則是一個(gè)更巧妙的 equals()。 在Groovy中要想比較對(duì)象的引用,不能用 ==,而應(yīng)該用 a.is(b)


Groovy與Java項(xiàng)目集成使用

項(xiàng)目中引入groovy依賴

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

常見(jiàn)的集成機(jī)制:

GroovyShell

GroovyClassLoader

GroovyScriptEngine

JSR 223 javax.script API

GroovyShell

GroovyShell允許在Java類中(甚至Groovy類)求任意Groovy表達(dá)式的值。您可使用Binding對(duì)象輸入?yún)?shù)給表達(dá)式,并最終通過(guò)GroovyShell返回Groovy表達(dá)式的計(jì)算結(jié)果

解析為腳本(groovy.lang.Script)運(yùn)行

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

GroovyClassLoader

用 Groovy 的 GroovyClassLoader ,動(dòng)態(tài)地加載一個(gè)腳本并執(zhí)行它的行為。GroovyClassLoader是一個(gè)定制的類裝載器,負(fù)責(zé)解釋加載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 其上再增添一個(gè)能夠處理腳本依賴及重新加載的功能層, GroovyScriptEngine可以從指定的位置(文件系統(tǒng),URL,數(shù)據(jù)庫(kù),等等)加載Groovy腳本

你可以使用一個(gè)CLASSPATH集合(url或者路徑名稱)初始化GroovyScriptEngine,之后便可以讓它根據(jù)要求去執(zhí)行這些路徑中的Groovy腳本了.GroovyScriptEngine同樣可以跟蹤相互依賴的腳本,如果其中一個(gè)被依賴的腳本發(fā)生變更,則整個(gè)腳本樹(shù)都會(huì)被重新編譯和加載。

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

JSR-223

JSR-223 是 Java 中標(biāo)準(zhǔn)的腳本框架調(diào)用 API。從 Java 6 開(kāi)始引入進(jìn)來(lái),主要目用來(lái)提供一種常用框架,以便從 Java 中調(diào)用多種語(yǔ)言

ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
// 編譯成類
groovyEngine.compile(script)
// 直接執(zhí)行
groovyEngine.eval(script)


Groovy實(shí)現(xiàn)相關(guān)原理

groovy負(fù)責(zé)詞法、語(yǔ)法解析groovy文件,然后用ASM生成普通的java字節(jié)碼文件,供jvm使用。

Groovy代碼文件與class文件的對(duì)應(yīng)關(guān)系

作為基于JVM的語(yǔ)言,Groovy可以非常容易的和Java進(jìn)行互操作,但也需要編譯成class文件后才能運(yùn)行,所以了解Groovy代碼文件和class文件的對(duì)應(yīng)關(guān)系,有助于更好地理解Groovy的運(yùn)行方式和結(jié)構(gòu)。

對(duì)于沒(méi)有任何類定義

如果Groovy腳本文件里只有執(zhí)行代碼,沒(méi)有定義任何類(class),則編譯器會(huì)生成一個(gè)Script的子類,類名和腳本文件的文件名一樣,而腳本的代碼會(huì)被包含在一個(gè)名為run的方法中,同時(shí)還會(huì)生成一個(gè)main方法,作為整個(gè)腳本的入口。

對(duì)于僅有一個(gè)類

如果Groovy腳本文件里僅含有一個(gè)類,而這個(gè)類的名字又和腳本文件的名字一致,這種情況下就和Java是一樣的,即生成與所定義的類一致的class文件, Groovy類都會(huì)實(shí)現(xiàn)groovy.lang.GroovyObject接口。

對(duì)于多個(gè)類

如果Groovy腳本文件含有一個(gè)或多個(gè)類,groovy編譯器會(huì)很樂(lè)意地為每個(gè)類生成一個(gè)對(duì)應(yīng)的class文件。如果想直接執(zhí)行這個(gè)腳本,則腳本里的第一個(gè)類必須有一個(gè)static的main方法。

對(duì)于有定義類的腳本

如果Groovy腳本文件有執(zhí)行代碼, 并且有定義類, 那么所定義的類會(huì)生成對(duì)應(yīng)的class文件, 同時(shí), 腳本本身也會(huì)被編譯成一個(gè)Script的子類,類名和腳本文件的文件名一樣


Spring對(duì)Groovy以及動(dòng)態(tài)語(yǔ)言的支持

Spring 從2.0開(kāi)始支持將動(dòng)態(tài)語(yǔ)言集成到基于 Spring 的應(yīng)用程序中。Spring 開(kāi)箱即用地支持 Groovy、JRuby 和 BeanShell。以 Groovy、JRuby 或任何受支持的語(yǔ)言編寫(xiě)的應(yīng)用程序部分可以無(wú)縫地集成到 Spring 應(yīng)用程序中。應(yīng)用程序其他部分的代碼不需要知道或關(guān)心單個(gè) Spring bean 的實(shí)現(xiàn)語(yǔ)言。

動(dòng)態(tài)語(yǔ)言支持將 Spring 從一個(gè)以 Java 為中心的應(yīng)用程序框架改變成一個(gè)以 JVM 為中心的應(yīng)用程序框架

Spring 通過(guò) ScriptFactory 和 ScriptSource 接口支持動(dòng)態(tài)語(yǔ)言集成。ScriptFactory 接口定義用于創(chuàng)建和配置腳本 Spring bean 的機(jī)制。理論上,所有在 JVM 上運(yùn)行語(yǔ)言都受支持,因此可以選擇特定的語(yǔ)言來(lái)創(chuàng)建自己的實(shí)現(xiàn)。ScriptSource 定義 Spring 如何訪問(wèn)實(shí)際的腳本源代碼;例如,通過(guò)文件系統(tǒng), URL, 數(shù)據(jù)庫(kù)。

在使用基于 Groovy 的 bean 時(shí),則有幾種選擇:

  • 將 Groovy 類編譯成普通的 Java 類文件

  • 在一個(gè) .groovy 文件中定義 Groovy 類或腳本

  • 在 Spring 配置文件中以內(nèi)聯(lián)方式編寫(xiě) Groovy 腳本

  1. 配置編譯的 Groovy 類, 和Java一樣的用法, 定義groovy class, 使用<bean/>創(chuàng)建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. 配置來(lái)自 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"/>

實(shí)現(xiàn)過(guò)程:

Groovy 語(yǔ)言集成通過(guò) ScriptFactory 的 GroovyScriptFactory 實(shí)現(xiàn)得到支持

當(dāng) Spring 裝載應(yīng)用程序上下文時(shí),它首先創(chuàng)建工廠 bean(這里是GroovyScriptFactory 類型的bean)。然后,執(zhí)行 ScriptFactoryPostProcessor bean中的postProcessBeforeInstantiation方法,用實(shí)際的腳本對(duì)象替換所有的工廠 bean。

ScriptFactoryPostProcessor:

    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
        // 只處理ScriptFactory類型的bean
        if (!ScriptFactory.class.isAssignableFrom(beanClass)) {
            return null;
        }
        // ...
        // 加載并解析groovy代碼, 在scriptBeanFactory中注冊(cè)BeanDefinition
        prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName);
        // ...
    }


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

        GenericBeanDefinition objectBd = new GenericBeanDefinition(bd);
        objectBd.setFactoryBeanName(scriptFactoryBeanName);
        // 指定工廠方法, ScriptFactory.getScriptedObject, 創(chuàng)建腳本的Java對(duì)象 
        objectBd.setFactoryMethodName("getScriptedObject");
        objectBd.getConstructorArgumentValues().clear();
        objectBd.getConstructorArgumentValues().addIndexedArgumentValue(0, scriptSource);
        objectBd.getConstructorArgumentValues().addIndexedArgumentValue(1, interfaces);
        return objectBd;
    }

創(chuàng)建bean的時(shí)候, SimpleInstantiationStrategy.instantiate

                 // 調(diào)用工廠方法創(chuàng)建beanInstance
                Object result = factoryMethod.invoke(factoryBean, args);
                if (result == null) {
                    result = new NullBean();
                }

GroovyScriptFactory.getScriptedObject

                      // 通過(guò)groovyClassLoader 加載并解析類
                    this.scriptClass = getGroovyClassLoader().parseClass(                           scriptSource.getScriptAsString(), scriptSource.suggestedClassName());

                    if (Script.class.isAssignableFrom(this.scriptClass)) {
                          // 如果是groovy 腳本, 那么運(yùn)行腳本, 將結(jié)果的類作為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 是一個(gè)回調(diào),Spring 在創(chuàng)建一個(gè) Groovy bean 之后會(huì)調(diào)用它。可以對(duì)一個(gè) Groovy bean 應(yīng)用附加的邏輯,或者執(zhí)行元編程
            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保存了所有通過(guò)腳本創(chuàng)建的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參數(shù)
<lang:groovy id="refresh"  refresh-check-delay="1000"
                 script-source="classpath:script/RefreshBean.groovy">
    </lang:groovy>

創(chuàng)建的是JdkDynamicAopProxy代理對(duì)象, 在每一次調(diào)用這個(gè)代理對(duì)象的方法的時(shí)候, 都回去校驗(yàn)被代理對(duì)象是否需要刷新, 通過(guò)比對(duì)腳本文件的最后更新時(shí)間和設(shè)定的更新時(shí)間間隔, 如果需要刷新則重新加載這個(gè)groovy文件, 并編譯, 然后創(chuàng)建一個(gè)新的bean并注冊(cè)進(jìn)行替換

3.內(nèi)聯(lián)方式配置

inline script標(biāo)簽, 從配置中讀取源代碼

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

綜上, 擴(kuò)展一下, 脫離xml配置, 可以從數(shù)據(jù)庫(kù)中定時(shí)加載groovy代碼, 構(gòu)建/更新/刪除BeanDefinition


Groovy運(yùn)行沙盒

沙盒原理也叫沙箱,英文sandbox。在計(jì)算機(jī)領(lǐng)域指一種虛擬技術(shù),且多用于計(jì)算機(jī)安全技術(shù)。安全軟件可以先讓它在沙盒中運(yùn)行,如果含有惡意行為,則禁止程序的進(jìn)一步運(yùn)行,而這不會(huì)對(duì)系統(tǒng)造成任何危害。

舉個(gè)例子:

docker容器可以理解為在沙盒中運(yùn)行的進(jìn)程。這個(gè)沙盒包含了該進(jìn)程運(yùn)行所必須的資源。不同的容器之間相互隔離。CGroup實(shí)現(xiàn)資源控制, Namespace實(shí)現(xiàn)訪問(wèn)隔離, rootfs實(shí)現(xiàn)文件系統(tǒng)隔離。


對(duì)于嵌入Groovy的Java系統(tǒng), 如果暴露接口, 可能存在的隱患有

  • 通過(guò)Java的Runtime.getRuntime().exec()方法執(zhí)行shell, 操作服務(wù)器.....

  • 執(zhí)行System.exit(0)

  • dump 內(nèi)存中的Class, 修改內(nèi)存中的緩存數(shù)據(jù)

ElasticSearch Groovy 腳本 遠(yuǎn)程代碼執(zhí)行漏洞


Groovy提供了編譯自定義器(Compilation customizers), 無(wú)論你使用 groovyc 還是采用 GroovyShell 來(lái)編譯類,要想執(zhí)行腳本,實(shí)際上都會(huì)使用到編譯器配置compiler configuration)信息。這種配置信息保存了源編碼或類路徑這樣的信息,而且還用于執(zhí)行更多的操作,比如默認(rèn)添加導(dǎo)入,顯式使用 AST(語(yǔ)法樹(shù)) 轉(zhuǎn)換,或者禁止全局 AST 轉(zhuǎn)換, 編譯自定義器的目標(biāo)在于使這些常見(jiàn)任務(wù)易于實(shí)現(xiàn)。CompilerConfiguration 類就是切入點(diǎn)。


groovy sandbox的實(shí)現(xiàn) -> https://github.com/jenkinsci/groovy-sandbox

實(shí)現(xiàn)過(guò)程:

groovy-sandbox實(shí)現(xiàn)了一個(gè)SandboxTransformer, 擴(kuò)展自CompilationCustomizer, 在Groovy代碼編譯時(shí)進(jìn)行轉(zhuǎn)換. 腳本轉(zhuǎn)換后, 讓腳本執(zhí)行的每一步都會(huì)被攔截, 調(diào)用Checker進(jìn)行檢查

可攔截所有內(nèi)容,包括

  • 方法調(diào)用(實(shí)例方法和靜態(tài)方法)
  • 對(duì)象分配(即除了“this(...)”和“super(...)”之外的構(gòu)造函數(shù)調(diào)用
  • 屬性訪問(wèn)(例如,z = foo.bar,z = foo?!癰ar”)和賦值(例如,foo.bar = z,foo?!癰ar”= z)
  • 數(shù)組訪問(wèn)和賦值

當(dāng)然, 執(zhí)行性能也會(huì)受到一些的影響

示例: Jenkins Pipline支持在Groovy沙盒中執(zhí)行Groovy腳本


image.png


其他:

Groovy元編程 原文 譯文

Groovy的ClassLoader體系

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容