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


    更多SpringBoot轮子导航

    SpringBoot项目实现日志打印SQL明细(包括SQL语句和参数)几种方式
    SpringBoot中几种好用的代码生成器(基于Mybatis-plus生成entity、mapper、xml等)
    SpringBoot整合Groovy脚本,实现动态编程
    SpringBoot整合ip2region实现使用ip监控用户访问城市
    SpringBoot整合EasyExcel实现Excel表格的导出功能
    SpringBoot整合阿里云OSS,支持文件上传、下载、删除、加签等操作
    SpringBoot整合aspectj实现面向切面编程(即AOP)
    SpringBoot整合Swagger2实现接口文档
    SpringBoot整合阿里云SchedulerX分布式任务调度组件
    SpringBoot整合kaptcha实现图片验证码功能

    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.groovygroupId>
                <artifactId>groovy-allartifactId>
                <version>2.4.7version>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    1、HelloWorld.groovy脚本代码

    package groovy
    
    def HelloWorld(){
        println "hello world"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    3、运行结果

    在这里插入图片描述

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

    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    3、运行结果

    在这里插入图片描述

    第四步、启动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 
         * @return
         */
        public static <T> T getBean(Class<T> clazz) {
            return getApplicationContext().getBean(clazz);
        }
    
        /**
         * 通过name,以及Clazz返回指定的Bean
         *
         * @param name
         * @param clazz
         * @param 
         * @return
         */
        public static <T> T getBean(String name, Class<T> clazz) {
            return getApplicationContext().getBean(name, clazz);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    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脚本调用");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

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

    在这里插入图片描述

    注意!!!

    通过第四步中我们可以看到,在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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这种用法肯定是不对的,这相当于每次调用这个方法都创建了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<>();
    }
    
    • 1
    • 2
    • 3

    第二次改造,增加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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

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

  • 相关阅读:
    【SQL Server】入门教程-基础篇(完结)
    浅议.NET遗留应用改造
    mysql之数据库账户管理与优化
    Hadoop3教程(二十):MapReduce的工作机制总结
    Linux相关理论——Linux
    Spring Boot 内置工具类应有尽有 建议收藏
    LeetCode加油站(贪心算法/暴力,分析其时间和空间复杂度)
    编译器(Compiler)及C/C++编译器安装(c+安装)
    根据mysql的执行顺序来写select
    CDQ分治模板
  • 原文地址:https://blog.csdn.net/weixin_33005117/article/details/126712394