本文首發於簡書 https://www.jianshu.com/p/2c6b95097b2c
簡介
Groovy是構建在JVM上的一個輕量級卻強大的動態語言, 它結合了Python、Ruby和Smalltalk的許多強大的特性.
Groovy就是用Java寫的 , Groovy語法與Java語法類似, Groovy 代碼能夠與 Java 代碼很好地結合,也能用於擴展現有代碼, 相對於Java, 它在編寫代碼的靈活性上有非常明顯的提升,Groovy 可以使用其他 Java 語言編寫的庫.
使用
- Groovy Console
- 安裝IDEA groovy插件
應用
ElasticSearch, Jenkins 都支持執行Groovy腳本
項目構建工具Gradle就是Groovy實現的
Groovy語法特性(相比於Java)
-
不需要分號
-
return
關鍵字可省略, 方法的最後一句表達式可作爲返回值返回 (視具體情況使用, 避免降低可讀性) -
類的默認作用域是
public
, 不需要getter/setter方法 -
def
關鍵字定義的變量類型都是Object, 任何變量, 方法都能用def
定義/聲明 , 在 Groovy 中 “一切都是對象 " -
導航操作符 ( ?. )可幫助實現對象引用不爲空時方法纔會被調用
// java if (object != null) { object.getFieldA(); } // groovy object?.getFieldA()
-
命令鏈, 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"
-
閉包. 閉包是一個短的匿名代碼塊。每個閉包會被編譯成繼承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")
-
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) }
-
數據結構的原生語法, 寫法更便捷
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 }
-
Groovy Truth
所有類型都能轉成布爾值,比如null
, void
對象, 等同於 0 或空的值,都會解析爲false
,其他則相當於true
-
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
-
Java 的
==
實際相當於 Groovy 的is()
方法,而 Groovy 的==
則是一個更巧妙的equals()
。 在Groovy中要想比較對象的引用,不能用==
,而應該用a.is(b)
- http://www.groovy-lang.org/syntax.html
- Differences with Java: http://www.groovy-lang.org/differences.html
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 腳本
- 配置編譯的 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();
-
配置來自 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, 修改內存中的緩存數據
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腳本
其他: