import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyShell;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.kohsuke.groovy.sandbox.SandboxTransformer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* //Groovy 工具類: 用於在 java 中呼叫 groovy 指令碼
* //在 java 中呼叫 groovy 指令碼,有 4 種方式,這裡採用的是 GroovyClassLoader 的方式
* // 1. ScriptEngineManager
* // 2. GroovyShell
* // 3. GroovyClassLoader
* // 4. GroovyScriptEngine
* //補充:
* // 1. groovy 指令碼中不能透過 @Autowired 注入 springBean,可以透過 cn.hutool.extra.spring.SpringUtil#getBean(java.lang.Class) 的方式來使用 springBean
* //參考:
* // https://blog.csdn.net/feiqinbushizheng/article/details/108579530
* // https://zhuanlan.zhihu.com/p/395699884
* // http://www.xwood.net/_site_domain_/_root/5870/5874/t_c279152.html
*/
public class GroovyUtil {
// 注意:
// 系統每執行一次指令碼,都會生成一個指令碼的 Class 物件,這個 Class 物件的名字由 "script" + System.currentTimeMillis()+Math.abs(text.hashCode() 組成,即使是相同的指令碼,
// 也會當做新的程式碼進行編譯、載入,會導致 Metaspace 的膨脹,隨著系統不斷地執行 Groovy 指令碼,最終導致 Metaspace 溢位
// 最佳化:
// 快取 groovyClassLoader.parseClass 返回的物件,這樣可以: 1. 解決 Metaspace 爆滿的問題;2. 因為不需要在執行時編譯載入,所以可以加快指令碼執行的速度
//
private static final ConcurrentHashMap<String, Class> code2classMap = new ConcurrentHashMap<>();
// GroovyClassLoader 例項常駐記憶體,增加處理的吞吐量
public static GroovyClassLoader groovyClassLoader;
static {
/** --------- 沙箱環境配置 --------- */
// 編碼規範和流程規範參考: https://zhuanlan.zhihu.com/p/395699884
// Groovy 會自動引入 java.util,java.lang 包,方便使用者呼叫的同時,也增加了系統的風險.為了防止使用者呼叫 System.exit 或 Runtime 等方法
// 導致系統當機,以及自定義的指令碼程式碼執行死迴圈或呼叫資源超時等問題,可以透過 SecureASTCustomizer,SandboxTransformer 對指令碼進行檢查
//
// <dependency>
// <groupId>org.kohsuke</groupId>
// <artifactId>groovy-sandbox</artifactId>
// <version>1.6</version>
// </dependency>
// 沙箱環境,若報錯 java.lang.VerifyError ... Illegal use of nonvirtual function call,則更改 groovy 依賴版本為 2.4.7
// <dependency>
// <groupId>org.codehaus.groovy</groupId>
// <artifactId>groovy-all</artifactId>
// <version>2.4.7</version>
// </dependency>
CompilerConfiguration config = new CompilerConfiguration();
config.addCompilationCustomizers(new SandboxTransformer());
/** --------- 沙箱環境配置 --------- */
groovyClassLoader = new GroovyClassLoader(GroovyUtil.class.getClassLoader(), config);
}
/**
* 載入 GroovyObject 快取(更新資料庫中的 groovy 指令碼後,需要呼叫該方法更新快取)
*
* @param script 指令碼原始碼
* @param scriptCode 指令碼唯一編號
*/
public static void loadCache(String script, String scriptCode) {
Class groovyClass = groovyClassLoader.parseClass(script, scriptCode);
code2classMap.put(scriptCode, groovyClass);
}
/**
* 刪除快取(刪除資料庫中的 groovy 指令碼後,需要呼叫該方法刪除快取)
*
* @param scriptCode 指令碼唯一編號
*/
public static void removeCache(String scriptCode) {
code2classMap.remove(scriptCode);
}
/**
* 透過反射呼叫 groovy 指令碼中定義的方法
*
* @param scriptCode 指令碼唯一編號
* @param methodName 方法名稱
* @param parameter 方法入參,引數型別和 groovy 指令碼中定義的方法的入參型別需保持一致
* @return 呼叫 groovy 指令碼中定義的方法的通用方法,為方便處理,這裡將出參都強轉為 Map<String, Object>,因此 groovy 中定義的方法出參,
* 也應該是 Map<String, Object>,否則會型別轉換失敗
*/
public static Map<String, Object> invokeMethod(String scriptCode, String methodName, Object parameter) {
Class clazz = code2classMap.get(scriptCode);
if (clazz == null) {
throw new RuntimeException("clazz is null");
}
Map<String, Object> result = new HashMap<>();
try {
// 將攔截器註冊到當前執行緒
new NoSystemExitInterceptor().register();
new NoRunTimeInterceptor().register();
GroovyObject groovyObject = (GroovyObject) clazz.newInstance();
result = (Map<String, Object>) groovyObject.invokeMethod(methodName, parameter);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
}
/**
* 檢查 groovy 語法是否正確
*
* @param sourceCode groovy 原始碼
*/
public static void checkGrammar(String sourceCode) {
GroovyShell groovyShell = new GroovyShell();
try {
groovyShell.parse(sourceCode);
} catch (CompilationFailedException e) {
throw new RuntimeException(e.getMessage());
}
}
}
點選檢視程式碼
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
@Component
public class GroovyService {
/**
* 初始化 GroovyObject 快取
*
* @throws Exception
*/
//@PostConstruct
public void initCache() {
/**
* 模擬去資料庫查詢所有的 groovy 程式碼
*/
List<GroovyCodeEntity> groovyCodeEntities = new ArrayList<>();
// demo1
GroovyCodeEntity groovyCodeEntity_1 = new GroovyCodeEntity();
groovyCodeEntity_1.setId(1L);
groovyCodeEntity_1.setCode("demo1");
groovyCodeEntity_1.setName("示例1");
String scriptOfRefJavaAndGroovy =
" import java.time.LocalDate \n" +
"import com.lullaby.embg.groovy.demo.script.InsertData \n" +
" \n" +
"class RefJavaAndGroovy { \n" +
" \n" +
" void test(Map<String, Object> map) { \n" +
" // 引用 java 類 \n" +
" println(\"日期: \" + LocalDate.now()) \n" +
" // 引用 groovy 類 \n" +
" InsertData.insert() \n" +
" } \n" +
"} ";
groovyCodeEntity_1.setScript(scriptOfRefJavaAndGroovy);
groovyCodeEntities.add(groovyCodeEntity_1);
// demo2
GroovyCodeEntity groovyCodeEntity_2 = new GroovyCodeEntity();
groovyCodeEntity_2.setId(2L);
groovyCodeEntity_2.setCode("demo2");
groovyCodeEntity_2.setName("示例2");
// 建議: Groovy 指令碼里面儘量使用 Java 靜態型別,這樣可以減少 Groovy 動態型別檢查等,提高編譯和載入 Groovy 指令碼的效率
String scriptOfRefSpringBean =
" import cn.hutool.extra.spring.SpringUtil \n" +
"import com.lullaby.embg.groovy.demo.MyService \n" +
" \n" +
"class RefSpringBean { \n" +
" \n" +
" void test(Map<String, Object> map) { \n" +
" println(\"引數: \" + map) \n" +
" MyService myService = SpringUtil.getBean(MyService.class) \n" +
" myService.doSth() \n" +
" } \n" +
"} \n";
groovyCodeEntity_2.setScript(scriptOfRefSpringBean);
groovyCodeEntities.add(groovyCodeEntity_2);
// demo3
GroovyCodeEntity groovyCodeEntity_3 = new GroovyCodeEntity();
groovyCodeEntity_3.setId(3L);
groovyCodeEntity_3.setCode("demo3");
groovyCodeEntity_3.setName("示例3");
String scriptOfInsertData =
" import groovy.sql.Sql \n" +
" \n" +
"class InsertData { \n" +
" \n" +
" static void insert() { \n" +
" \n" +
" Sql sql = Sql.newInstance(\"jdbc:mysql://127.0.0.1:3306/demo\", \"user\", \"password\") \n" +
" \n" +
" sql.executeInsert(\"INSERT INTO tb_demo (id, code, name, other_info) VALUES (?, ?, ?, ?)\" \n" +
" , [1L, \"code-1\", \"name-1\", \"one\"]) \n" +
" \n" +
" sql.close() \n" +
" } \n" +
" \n" +
"} \n";
groovyCodeEntity_3.setScript(scriptOfInsertData);
groovyCodeEntities.add(groovyCodeEntity_3);
for (GroovyCodeEntity groovyCodeEntity : groovyCodeEntities) {
GroovyUtil.loadCache(groovyCodeEntity.getScript(), groovyCodeEntity.getCode());
}
}
}
點選檢視程式碼
import cn.hutool.http.HttpUtil;
import org.junit.Test;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/groovy")
public class TestGroovyController {
/**
* 在 groovy 中呼叫 java 類/groovy 類
*/
@RequestMapping("/refJavaOrGroovy")
@Transactional
public String refJavaOrGroovy() {
// 指令碼原始碼參看 GroovyService.initCache
GroovyUtil.invokeMethod("demo1", "test", null);
return "refGroovy 呼叫了";
}
/**
* 在 groovy 中呼叫 spring-bean
*/
@RequestMapping("/refSpringBean")
@Transactional
public String refSpringBean() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("a", "A");
parameters.put("b", "B");
// 指令碼原始碼參看 GroovyService.initCache
GroovyUtil.invokeMethod("demo2", "test", parameters);
return "refSpringBean 呼叫了";
}
/**
* 事務(不支援在指令碼之外控制事務)
*
* @return
*/
@RequestMapping("/tx")
@Transactional
public String tx() {
// 指令碼原始碼參看 GroovyService.initCache
GroovyUtil.invokeMethod("demo3", "insert", null);
// 這裡拋異常,並不會使得指令碼中的事務回滾
if (1 == 1) {
throw new RuntimeException();
}
return "tx 呼叫了";
}
@Test
public void refJavaOrGroovyTest() {
String result = HttpUtil.get("http://localhost:8080/groovy/refJavaOrGroovy");
System.err.println(result);
}
@Test
public void refSpringBeanTest() {
String result = HttpUtil.get("http://localhost:8080/groovy/refSpringBean");
System.err.println(result);
}
@Test
public void txTest() {
String result = HttpUtil.get("http://localhost:8080/groovy/tx");
System.err.println(result);
}
}
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void doSth() {
System.err.println(">>>>>>>>> service方法執行了");
}
}
import java.util.stream.Collectors
/**
* 閉包 "{}"
*/
class ClosureTest {
static void main(String[] args) {
List<String> list = Arrays.asList("a1", "a2", "b1", "b2")
// Finds the first value matching the closure condition.
String element = list.find { it -> it.startsWith("a") }
println("element: " + element)
List<String> elements = list.stream().filter { it -> it.startsWith("a") }.collect(Collectors.toList())
println("elements: " + elements)
}
}
點選檢視程式碼
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
public class NoRunTimeInterceptor extends GroovyInterceptor {
@Override
public Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable {
if (receiver == Runtime.class) {
throw new SecurityException("不要在指令碼中呼叫 RunTime 類的方法");
}
return super.onStaticCall(invoker, receiver, method, args);
}
}
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
public class NoSystemExitInterceptor extends GroovyInterceptor {
@Override
public Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable {
if (receiver == System.class && method.equals("exit")) {
throw new SecurityException("不要在指令碼中呼叫 System.exit(int status) 方法");
}
return super.onStaticCall(invoker, receiver, method, args);
}
}