Groovy簡介與使用

本文首發於簡書 https://www.jianshu.com/p/2c6b95097b2c

簡介

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"
    
  7. 閉包. 閉包是一個短的匿名代碼塊。每個閉包會被編譯成繼承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")
    
  8. 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)
    }
    
  9. 數據結構的原生語法, 寫法更便捷

    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
    }
    
  10. 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
    
  2. 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體系

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章