美文网首页程序员@IT·互联网
SpringBoot整合Groovy脚本,实现动态编程

SpringBoot整合Groovy脚本,实现动态编程

作者: sum墨 | 来源:发表于2024-04-26 09:44 被阅读0次

    Groovy简介

    Groovy 是增强 Java 平台的唯一的脚本语言。它提供了类似于 Java 的语法,内置映射(Map)、列表(List)、方法、类、闭包(closure)以及生成器。脚本语言不会替代系统编程语言,两者是相互补充的。

    大名鼎鼎的 Gradle,背后是 Groovy。Spring 的未来越来越多的使用 Groovy,甚至在用 Jira 跟踪项目时,背后也有 Groovy。实际上,就应用场景而言,Java 开发已经有越来越多的 Groovy 出现在后台了。而对于一般的应用开发,只要能用 Java 就都能用到 Groovy,唯一的难点只在于能不能招到足够的人员。

    应用场景

    • 连接已有的组件
    • 处理经常变化的多种类型的实体
    • 具有图形化用户界面
    • 拥有快速变化的功能

    Groovy脚本的基础概念请移步

    Groovy 简介

    集成与使用

    那么接下来介绍SpringBoot如何集成Groovy脚本,并应用到实际开发中。

    第一步、与SpringBoot集成

    1、pom.xml文件如下:

      <dependency>
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy-all</artifactId>
                <version>2.4.7</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    

    第二步、写出Groovy版本的“Hello World”

    1、HelloWorld.groovy脚本代码

    package groovy
    
    def HelloWorld(){
        println "hello world"
    }
    

    2、创建测试类GroovyTest.java

    package com.example.springbootgroovy.service;
    
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    
    /**
     * 这个是Groovy的第一个小程序,脚本为:
     * 
     package groovy
     
     def helloworld(){
      println "hello world"
     }
     *
     */
    public class GroovyTest {
    
        public static void main(String[] args) throws Exception {
            //创建GroovyShell
            GroovyShell groovyShell = new GroovyShell();
            //装载解析脚本代码
            Script script = groovyShell.parse("package groovy\n" +
                    "\n" +
                    "def HelloWorld(){\n" +
                    "    println \"hello world\"\n" +
                    "}");
            //执行
            script.invokeMethod("HelloWorld", null);
        }
    }
    
    

    3、运行结果

    image.png

    第三步、传入变量与获取返回值

    1、变量与返回值Groovy脚本代码

    package groovy
    
    /**
     * 简易加法
     * @param a 数字a
     * @param b 数字b
     * @return 和
     */
    def add(int a, int b) {
        return a + b
    }
    
    /**
     * map转化为String
     * @param paramMap 参数map
     * @return 字符串
     */
    def mapToString(Map<String, String> paramMap) {
        StringBuilder stringBuilder = new StringBuilder();
        paramMap.forEach({ key, value ->
            stringBuilder.append("key:" + key + ";value:" + value)
        })
        return stringBuilder.toString()
    }
    

    2、创建测试类GroovyTest2.java

    package com.example.springbootgroovy.service;
    
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 向Groovy脚本中传入变量,以及获取返回值
     */
    public class GroovyTest2 {
        public static void main(String[] args) {
            //创建GroovyShell
            GroovyShell groovyShell = new GroovyShell();
            //装载解析脚本代码
            Script script = groovyShell.parse("package groovy\n" +
                    "\n" +
                    "/**\n" +
                    " * 简易加法\n" +
                    " * @param a 数字a\n" +
                    " * @param b 数字b\n" +
                    " * @return 和\n" +
                    " */\n" +
                    "def add(int a, int b) {\n" +
                    "    return a + b\n" +
                    "}\n" +
                    "\n" +
                    "/**\n" +
                    " * map转化为String\n" +
                    " * @param paramMap 参数map\n" +
                    " * @return 字符串\n" +
                    " */\n" +
                    "def mapToString(Map<String, String> paramMap) {\n" +
                    "    StringBuilder stringBuilder = new StringBuilder();\n" +
                    "    paramMap.forEach({ key, value ->\n" +
                    "        stringBuilder.append(\"key:\" + key + \";value:\" + value)\n" +
                    "    })\n" +
                    "    return stringBuilder.toString()\n" +
                    "}");
            //执行加法脚本
            Object[] params1 = new Object[]{1, 2};
            int sum = (int) script.invokeMethod("add", params1);
            System.out.println("a加b的和为:" + sum);
            //执行解析脚本
            Map<String, String> paramMap = new HashMap<>();
            paramMap.put("科目1", "语文");
            paramMap.put("科目2", "数学");
            Object[] params2 = new Object[]{paramMap};
            String result = (String) script.invokeMethod("mapToString", params2);
            System.out.println("mapToString:" + result);
        }
    }
    

    3、运行结果

    image.png

    第四步、启动SpringBoot,在Groovy脚本中通过SpringContextUtil获取SpringBoot容器中的Bean

    1、创建SpringContextUtil.java

    package com.example.springbootgroovy.util;
    
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    /**
     * Spring上下文获取
     */
    @Component
    public class SpringContextUtil implements ApplicationContextAware {
    
        private static ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringContextUtil.applicationContext = applicationContext;
        }
    
        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
        /**
         * 通过name获取 Bean.
         *
         * @param name
         * @return
         */
        public static Object getBean(String name) {
            return getApplicationContext().getBean(name);
        }
    
        /**
         * 通过class获取Bean.
         *
         * @param clazz
         * @param <T>
         * @return
         */
        public static <T> T getBean(Class<T> clazz) {
            return getApplicationContext().getBean(clazz);
        }
    
        /**
         * 通过name,以及Clazz返回指定的Bean
         *
         * @param name
         * @param clazz
         * @param <T>
         * @return
         */
        public static <T> T getBean(String name, Class<T> clazz) {
            return getApplicationContext().getBean(name, clazz);
        }
    }
    

    2、创建GroovyTestService.java,并加上@Service注解加入到SpringBoot容器中

    package com.example.springbootgroovy.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class GroovyTestService {
    
        public void test(){
            System.out.println("我是SpringBoot框架的成员类,但该方法由Groovy脚本调用");
        }
    
    }
    

    3、Groovy脚本如下

    package groovy
    
    import com.example.springbootgroovy.service.GroovyTestService
    import com.example.springbootgroovy.util.SpringContextUtil
    
    /**
     * 静态变量
     */
    class Globals {
        static String PARAM1 = "静态变量"
        static int[] arrayList = [1, 2]
    }
    
    def getBean() {
        GroovyTestService groovyTestService = SpringContextUtil.getBean(GroovyTestService.class);
        groovyTestService.test()
    }
    

    4、启动类代码如下

    package com.example.springbootgroovy;
    
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/groovy")
    @SpringBootApplication
    public class SpringBootGroovyApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootGroovyApplication.class, args);
        }
    
        @RequestMapping("/test")
        public String test() {
            //创建GroovyShell
            GroovyShell groovyShell = new GroovyShell();
            //装载解析脚本代码
            Script script = groovyShell.parse("package groovy\n" +
                    "\n" +
                    "import com.example.springbootgroovy.service.GroovyTestService\n" +
                    "import com.example.springbootgroovy.util.SpringContextUtil\n" +
                    "\n" +
                    "/**\n" +
                    " * 静态变量\n" +
                    " */\n" +
                    "class Globals {\n" +
                    "    static String PARAM1 = \"静态变量\"\n" +
                    "    static int[] arrayList = [1, 2]\n" +
                    "}\n" +
                    "\n" +
                    "def getBean() {\n" +
                    "    GroovyTestService groovyTestService = SpringContextUtil.getBean(GroovyTestService.class);\n" +
                    "    groovyTestService.test()\n" +
                    "}");
            //执行
            script.invokeMethod("getBean", null);
            return "ok";
        }
    }
    
    

    5、启动后调用接口:http://localhost:8080/groovy/test,运行结果如下

    image.png

    注意!!!

    通过第四步中我们可以看到,在Groovy中是可以获取到SpringBoot容器对象的。虽然很方便,但是很危险。如果没有做好权限控制,Groovy脚本将会成为攻击你系统最有力的武器!!!

    另外Groovy脚本用不好,会导致OOM,最终服务器宕机

    我最开始的用法

       public static List<JSONObject> invokeMethod(String templateScript, JSONObject configParam) {
            Binding groovyBinding = new Binding();
            GroovyShell groovyShell = new GroovyShell(groovyBinding);
            Script script = groovyShell.parse(templateScript);
            Object[] params = new Object[]{configParam};
            List<JSONObject> resultList = (List<JSONObject>) script.invokeMethod("methodName", params);
            return resultList;
        }
    

    这种用法肯定是不对的,这相当于每次调用这个方法都创建了GroovyShell、Script等实例,随着调用次数的增加,必然会出现OOM。

    第一次改造,在方法最后增加一行:groovyShell.getClassLoader().clearCache();

    也就是在方法的最后调用一次clearCache方法,这样可以清除掉GroovyShell、Script等实例,但是还是不够。导致OOM的原因并不止GroovyShell、Script等实例过多,经过查阅资料得知,如果脚本中的Java代码也创建了对象或者new了实例,即使销毁了GroovyShell也不会销毁脚本中的对象

    例如下面这个脚本,会创建一个ArrayList对象。这个对象不会随着GroovyShell、Script等实例的消失而消失,所以还是会有问题。

    def test(){
        List<String> list = new ArrayList<>();
    }
    

    第二次改造,增加SCRIPT_MAP,将已有的Groovy实例放入缓存中维护起来

    /**
         * 缓存Script,避免创建太多
         */
        private static final Map<String, Script> SCRIPT_MAP = Maps.newHashMap();
    
        private static final GroovyClassLoader CLASS_LOADER = new GroovyClassLoader();
    
        public static Script loadScript(String key, String rule) {
            if (SCRIPT_MAP.containsKey(key)) {
                return SCRIPT_MAP.get(key);
            }
            Script script = loadScript(rule, new Binding());
            SCRIPT_MAP.put(key, script);
            return script;
        }
    
    
        public static Script loadScript(String rule, Binding binding) {
            if (StringUtils.isEmpty(rule)) {
                return null;
            }
            try {
                Class ruleClazz = CLASS_LOADER.parseClass(rule);
                if (ruleClazz != null) {
                    log.info("load rule:" + rule + " success!");
                    return InvokerHelper.createScript(ruleClazz, binding);
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                CLASS_LOADER.clearCache();
            }
            return null;
        }
    

    这种方法的好处是解决了OOM问题,但也有一个问题,如果脚本内容修改了的话,需要清空SCRIPT_MAP,重新装载脚本实例。

    文末小彩蛋,自己花一个星期做的小网站,放出来给大家看看,网址如下:http://47.120.49.119:8080

    相关文章

      网友评论

        本文标题:SpringBoot整合Groovy脚本,实现动态编程

        本文链接:https://www.haomeiwen.com/subject/bpkoxjtx.html