groovy


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

posted @   凛冬雪夜  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示