groovy

凛冬雪夜發表於2024-10-15

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);
    }
}

相關文章