Groovy系列三 Java SpringBoot 整合 Groovy
作者:mmseoamin日期:2023-12-21

 

目录

一、概述

一、在Java中使用Groovy:

二、在Groovy中使用Java:

三、几种范式的不同、优缺点

Java调用Groovy的类和方法

Groovy调用Java的类和方法:

 使用GroovyShell执行Groovy脚本

使用GroovyClassLoader加载和执行Groovy脚本:

二、实战

第一步、与SpringBoot集成,引入依赖

第二步、编写groovy脚本

第三步、创建测试类使用GroovyShell演示

第四步、查看运行结果

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

第六步、创建容器中的Bean

第七步、访问代码

第八步、启动、接口测试

三、优化

第一步、创建Groovy脚本,使用GroovyClassLoader实现

第二步、创建Groovy调用Bean

第三步、创建GroovyClassLoader加载类

第四步、创建请求API


Groovy系列三 Java SpringBoot 整合 Groovy,第1张

上两篇我们系统的学习了Groovy的基础语法和GDK,本篇咱们学习如何将Groovy和Java集成,并且学习集成到SpringBoot项目中。Groovy和Java之间有很好的集成,可以相互调用和使用对方的代码和特性。通过Groovy和Java的集成,可以充分发挥两者的优势,让开发更加灵活和高效。

一、概述

一、在Java中使用Groovy:

  1. 添加Groovy的依赖到Java项目中。
  2. 在Java代码中使用Groovy的类和脚本。Groovy代码可以直接在Java中执行,可以调用Groovy类的方法、访问其属性等。可以使用GroovyShell或GroovyClassLoader来执行Groovy脚本。

二、在Groovy中使用Java:

  1. Groovy天生支持Java,可以直接使用Java类、调用Java方法等。Groovy代码可以与Java代码混合编写。
  2. 在Groovy代码中使用Java类时,不需要额外的导入,直接使用即可。
  3. Groovy还提供了更简洁的语法和更强大的特性,例如闭包、扩展方法、动态类型等,可以更方便地编写代码。

为了实现更好的集成,可以注意以下几点:

  • 使用相同的依赖管理工具,以确保Java和Groovy项目使用的是相同的版本依赖。
  • 确保Java类被编译并将其生成的字节码文件和Groovy代码放在相同的classpath下,以便彼此访问。
  • 在使用GroovyShell或GroovyClassLoader执行Groovy脚本时,将Java类的路径包含在class path中,以便Groovy代码可以访问Java类。

    三、几种范式的不同、优缺点

    在Groovy和Java之间实现集成有多种方式,下面我会描述其中几种常见的方式,以及它们的不同、优缺点。

    Java调用Groovy的类和方法:

    • 描述:Java可以直接通过类路径访问Groovy的类和方法,将Groovy代码视为Java代码的一部分。可以调用Groovy类的方法、访问其属性等。
    • 不同:Java可以无缝地调用Groovy类和方法,就像调用Java代码一样。
    • 优点:简单直接,Groovy与Java代码混合编写非常方便。
    • 缺点:对于Groovy独有的特性,如闭包、动态类型等,Java可能无法完全理解。

      Groovy调用Java的类和方法:

      • 描述:Groovy天生支持Java,可以直接使用Java类、调用Java方法等。Groovy代码可以与Java代码混合编写,无需额外导入。
      • 不同:Groovy与Java的集成非常融洽,可以自动导入Java类,直接使用Java的语法。
      • 优点:无缝集成,可以充分利用Java的生态系统和已有的库。
      • 缺点:Groovy在某些方面可能比Java更“动态”,这意味着在一些情况下可能会有性能和类型安全性的损失。

         使用GroovyShell执行Groovy脚本:

        • 描述:在Java代码中使用GroovyShell执行Groovy脚本代码块。可以动态加载和执行Groovy脚本。
        • 不同:通过GroovyShell的evaluate方法,可以在Java中执行动态的Groovy脚本代码。
        • 优点:能够在运行时动态执行Groovy脚本,灵活性高,方便快捷。
        • 缺点:动态执行脚本可能会带来一定的性能影响,并且需要额外的语法检查。

          使用GroovyClassLoader加载和执行Groovy脚本:

          • 描述:在Java中通过GroovyClassLoader加载和执行Groovy脚本,可以实现更灵活的脚本执行。
          • 不同:通过GroovyClassLoader加载Groovy脚本,可以获得对应的Class对象,并根据需要进行实例化和调用。
          • 优点:可以灵活地加载和执行Groovy脚本,并与Java代码进行交互。缺点:相对于GroovyShell,使用GroovyClassLoader需要更多的代码来实现加载和执行。

            综上所述,不同的Groovy和Java集成方式具有不同的优缺点。我们可以根据具体需求选择合适的方式。使用Java调用Groovy类和方法以及Groovy调用Java类和方法是最直接、无缝的集成方式。而使用GroovyShell或GroovyClassLoader执行Groovy脚本则更灵活,适用于需要动态执行脚本的场景。

            二、实战

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

            第一步、与SpringBoot集成,引入依赖

            
               org.codehaus.groovy
               groovy-all
               3.0.17
               pom
            

            第二步、编写groovy脚本

            package script
            import com.example.groovy.GroovyInvokeJavaDemo
            import com.example.groovy.groovyshell.ShellGroovyDTO
            import com.example.groovy.utils.SpringContextUtil
            /**
             * @Author: lly
             * @Date: 2023/7/1
             */
            def helloWord() {
                return "hello groovy"
            }
            helloWord()
            def cal(int a, int b) {
                ShellGroovyDTO dto = new ShellGroovyDTO()
                dto.setA(a)
                dto.setB(b)
                if (b > 0) {
                    dto.setNum(a + b)
                } else {
                    dto.setNum(a)
                }
                return dto
            };
            cal(a, b)
            /** 定义静态变量 **/
            class Globals {
                static String PARAM1 = "静态变量"
                static int[] arrayList = [1, 2]
            }
            def groovyInvokeJavaMethod(int a, int b) {
                GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
            //    return groovyInvokeJavaDemo.groovyInvokeJava();
                return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b);
            }
            groovyInvokeJavaMethod(a, b)

            第三步、创建测试类使用GroovyShell演示

            * // 创建GroovyShell实例

            * // 创建Binding对象,用于传递参数和接收结果

            * // 设置参数

            * // 执行Groovy脚本

            * // 获取结果

            package com.example.groovy.groovyshell;
            import groovy.lang.Binding;
            import groovy.lang.GroovyShell;
            import groovy.lang.Script;
            /**
             * @Author: lly
             * @Date: 2023/7/1
             * 

            * 下面这段测试类包含两个,一个是有参数的调用,一个是无参数的调用 * // 创建GroovyShell实例 * // 创建Binding对象,用于传递参数和接收结果 * // 设置参数 * // 执行Groovy脚本 * // 获取结果 */ public class GroovyShellApp { /** * GroovyShell 无参数 demo **/ public static void main(String[] args) { String groovyStr = "package script\n" + "\n" + "import com.example.groovy.groovyshell.ShellGroovyDTO\n" + "\n" + "/**\n" + " * @Author: lly\n" + " * @Date: 2023/7/1\n" + " */\n" + "\n" + "def helloWord() {\n" + " return \"hello groovy\"\n" + "}\n" + "\n" + "helloWord()\n" + "\n" + "def cal(int a, int b) {\n" + " ShellGroovyDTO dto = new ShellGroovyDTO()\n" + " dto.setA(a)\n" + " dto.setB(b)\n" + " if (b > 0) {\n" + " dto.setNum(a + b)\n" + " } else {\n" + " dto.setNum(a)\n" + " }\n" + " return dto\n" + "};\n" + "\n" + "cal(a , b)"; // 创建GroovyShell实例 GroovyShell shell = new GroovyShell(); Script script = shell.parse(groovyStr); Object helloWord = script.invokeMethod("helloWord", null); System.out.println(helloWord); } /** GroovyShell 有参数 demo **/ // public static void main(String[] args) { // // String groovyStr = "package script\n" + // "\n" + // "import com.example.groovy.groovyshell.ShellGroovyDTO\n" + // "\n" + // "/**\n" + // " * @Author: lly\n" + // " * @Date: 2023/7/1\n" + // " */\n" + // "def cal(int a, int b) {\n" + // " ShellGroovyDTO dto = new ShellGroovyDTO()\n" + // " dto.setA(a)\n" + // " dto.setB(b)\n" + // " if (b > 0) {\n" + // " dto.setNum(a + b)\n" + // " } else {\n" + // " dto.setNum(a)\n" + // " }\n" + // " return dto\n" + // "};\n" + // "\n" + // "cal(a, b)"; // // // 创建Binding对象,用于传递参数和接收结果 // Binding binding = new Binding(); // // // 创建GroovyShell实例 // GroovyShell shell = new GroovyShell(binding); // // // // 设置参数 // binding.setVariable("a", 10); // binding.setVariable("b", 20); // // // 执行Groovy脚本 // Object result = shell.evaluate(groovyStr); // // // 获取结果 // ShellGroovyDTO dto = (ShellGroovyDTO) result; // System.out.println(dto); // } } package com.example.groovy.groovyshell; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @Author: lly * @Date: 2023/7/1 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class ShellGroovyDTO { private Integer a; private Integer b; private Integer num; }

            第四步、查看运行结果

            Groovy系列三 Java SpringBoot 整合 Groovy,第2张

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

            上面几步都是纯Java代码中调用Groovy,其实在开发的过程中,我们通常会Groovy和Java代码互调,接下来咱们看看如何使实现Groovy中通过SpringContextUtil获取SpringBoot容器中的Bean并且调用目标方法。

            package com.example.groovy.utils;
            import org.springframework.beans.BeansException;
            import org.springframework.context.ApplicationContext;
            import org.springframework.context.ApplicationContextAware;
            import org.springframework.stereotype.Service;
            /**
             * @Author: lly
             * @Date: 2023/7/2
             */
            @Service
            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 getBean(Class clazz) {
                    return getApplicationContext().getBean(clazz);
                }
                /**
                 * 通过name,以及Clazz返回指定的Bean
                 *
                 * @param name
                 * @param clazz
                 * @param 
                 * @return
                 */
                public static  T getBean(String name, Class clazz) {
                    return getApplicationContext().getBean(name, clazz);
                }
            }

            第六步、创建容器中的Bean

            咱们创建一个“GroovyInvokeJavaDemo“bean,并交给Spring管理。其中有两个目标方法,一个是需要参数的,一个不需要参数,需要参数的咱们通过Java调用Groovy的时候传入的参数在Groovy中调用Java方法的时候传入。

            package com.example.groovy;
            import lombok.extern.slf4j.Slf4j;
            import org.springframework.stereotype.Service;
            import java.util.ArrayList;
            import java.util.List;
            /**
             * @Author: lly
             * @Date: 2023/7/2
             */
            @Service
            @Slf4j
            public class GroovyInvokeJavaDemo {
                public String groovyInvokeJava() {
                    List lits = new ArrayList<>();
                    log.info("this is SpringBoot class, groovy script invoke this method ...");
                    return "this is SpringBoot class, groovy script invoke this method ...";
                }
                public String groovyInvokeJavaParam(int a, int b) {
                    List lits = new ArrayList<>();
                    log.info("this is SpringBoot class, groovy script invoke this method ,param is a:{}, b:{}", a, b);
                    return "this is SpringBoot class, groovy script invoke this method , a:" + a + ", b:" + b;
                }
            }

            第七步、访问代码

            package com.example.groovy;
            import com.example.groovy.classloader.GroovyClassLoaderRule;
            import groovy.lang.Binding;
            import groovy.lang.GroovyShell;
            import org.springframework.web.bind.annotation.RequestMapping;
            import org.springframework.web.bind.annotation.RestController;
            import javax.annotation.Resource;
            /**
             * @Author: lly
             * @Date: 2023/7/2
             */
            @RestController
            @RequestMapping("/groovy")
            public class GroovyInvokeJavaSpringController {
                @Resource
                private GroovyClassLoaderRule groovyClassLoaderRule;
                @RequestMapping("/groovy-shell/spring")
                public String groovyInvokeJavaMethodTest() {
                    String groovyStr = "package script\n" +
                            "\n" +
                            "import com.example.groovy.GroovyInvokeJavaDemo\n" +
                            "import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
                            "import com.example.groovy.utils.SpringContextUtil\n" +
                            "\n" +
                            "/**\n" +
                            " * @Author: lly\n" +
                            " * @Date: 2023/7/1\n" +
                            " */\n" +
                            "\n" +
                            "def helloWord() {\n" +
                            "    return \"hello groovy\"\n" +
                            "}\n" +
                            "\n" +
                            "helloWord()\n" +
                            "\n" +
                            "def cal(int a, int b) {\n" +
                            "    ShellGroovyDTO dto = new ShellGroovyDTO()\n" +
                            "    dto.setA(a)\n" +
                            "    dto.setB(b)\n" +
                            "    if (b > 0) {\n" +
                            "        dto.setNum(a + b)\n" +
                            "    } else {\n" +
                            "        dto.setNum(a)\n" +
                            "    }\n" +
                            "    return dto\n" +
                            "};\n" +
                            "\n" +
                            "cal(a , b)\n" +
                            "\n" +
                            "/** 定义静态变量 **/\n" +
                            "class Globals {\n" +
                            "    static String PARAM1 = \"静态变量\"\n" +
                            "    static int[] arrayList = [1, 2]\n" +
                            "}\n" +
                            "\n" +
                            "def groovyInvokeJavaMethod(int a, int b) {\n" +
                            "    GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
                            "//    return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
                            "    return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b);\n" +
                            "}\n" +
                            "\n" +
                            "groovyInvokeJavaMethod(a, b)";
                    Binding binding = new Binding();
                    binding.setVariable("a", 100);
                    binding.setVariable("b", 100);
                    GroovyShell groovyShell = new GroovyShell(binding);
                    Object evaluate = groovyShell.evaluate(groovyStr);
                    groovyShell.getClassLoader().clearCache();
                    return (String) evaluate;
                }
            }

            第八步、启动、接口测试

            访问“http://localhost:8080/groovy/groovy-shell/spring”即可看到效果。

            Groovy系列三 Java SpringBoot 整合 Groovy,第3张

            三、优化

            以上我们就将Groovy集成到Java中来了,但是上面的代码有很大的问题,主要体现在两个方面:

            第一个方面:通过第五步中我们可以看到,在Groovy中是可以获取到SpringBoot容器对象的,能拿到容器对象就可以获取到容器中所有的东西。这虽然很方便、灵活,但是非常的危险。如果没有做好权限控制,Groovy脚本将会成为攻击你系统最有力的武器!

            第二个方面:Groovy脚本用不好,会导致OOM,最终服务器宕机。每次调用这个方法都创建了GroovyShell、Script等实例,随着调用次数的增加,必然会出现OOM。

            解决方案是通过GroovyClassLoader的clearCache()函数在调用完毕销毁GroovyShell、Script等实例,但是其实这样仅仅是不够的,导致OOM的原因并不止GroovyShell、Script等实例过多,经过查阅资料得知,如果脚本中的Java代码也创建了对象或者new了实例,即使销毁了GroovyShell也不会销毁脚本中的对象。所以Groovy代码最好是使用缓存管理。

            第一步、创建Groovy脚本,使用GroovyClassLoader实现

            GroovyClassLoad_1.groovy

            package script
            import com.example.groovy.GroovyInvokeJavaDemo
            import com.example.groovy.groovyshell.ShellGroovyDTO
            import com.example.groovy.utils.SpringContextUtil
            /**
             * @Author: lly
             * @Date: 2023/7/1
             */
            def groovyInvokeJavaMethod(int a, int b) {
                GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
            //    return groovyInvokeJavaDemo.groovyInvokeJava();
                return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)
            }
            groovyInvokeJavaMethod(a, b)

            GroovyClassLoad_2.groovy

            package script
            import com.example.groovy.GroovyInvokeJavaDemo
            import com.example.groovy.groovyshell.ShellGroovyDTO
            import com.example.groovy.utils.SpringContextUtil
            /**
             * @Author: lly
             * @Date: 2023/7/1
             */
            def groovyInvokeJavaMethod(int a, int b) {
                GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
            //    return groovyInvokeJavaDemo.groovyInvokeJava();
                return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)
            }
            groovyInvokeJavaMethod(a, b)

            第二步、创建Groovy调用Bean

            package com.example.groovy;
            import lombok.extern.slf4j.Slf4j;
            import org.springframework.stereotype.Service;
            import java.util.ArrayList;
            import java.util.List;
            /**
             * @Author: lly
             * @Date: 2023/7/2
             */
            @Service
            @Slf4j
            public class GroovyInvokeJavaDemo {
                public String groovyInvokeJava() {
                    List lits = new ArrayList<>();
                    log.info("this is SpringBoot class, groovy script invoke this method ...");
                    return "this is SpringBoot class, groovy script invoke this method ...";
                }
                public String groovyInvokeJavaParam(int a, int b) {
                    List lits = new ArrayList<>();
                    log.info("this is SpringBoot class, groovy script invoke this method ,param is a:{}, b:{}", a, b);
                    return "this is SpringBoot class, groovy script invoke this method , a:" + a + ", b:" + b;
                }
            }

            第三步、创建GroovyClassLoader加载类

            package com.example.groovy.classloader;
            /**
             * @Author: lly
             * @Date: 2023/7/1
             *
             * 定义 Groovy 执行的接口
             */
            public interface GroovyClassLoaderRule {
                String run();
            }
            package com.example.groovy.classloader;
            import groovy.lang.Binding;
            import groovy.lang.GroovyClassLoader;
            import groovy.lang.GroovyObject;
            import groovy.lang.Script;
            import lombok.extern.slf4j.Slf4j;
            import org.apache.groovy.parser.antlr4.util.StringUtils;
            import org.codehaus.groovy.runtime.InvokerHelper;
            import org.springframework.stereotype.Service;
            import javax.annotation.Resource;
            import java.util.HashMap;
            import java.util.Map;
            /**
             * @Author: lly
             * @Date: 2023/7/1
             */
            @Slf4j
            @Service
            public class GroovyClassLoaderRuleImpl implements GroovyClassLoaderRule {
                /**
                 * 脚本容器 :缓存Script,避免创建太多
                 **/
                private static final Map SCRIPT_MAP = new HashMap<>();
                private static final GroovyClassLoader CLASS_LOADER = new GroovyClassLoader();
                public static GroovyObject loadScript(String key, String rule) {
                    if (SCRIPT_MAP.containsKey(key)) {
                        return SCRIPT_MAP.get(key);
                    }
                    GroovyObject groovyObject = loadScript(rule);
                    SCRIPT_MAP.put(key, groovyObject);
                    return groovyObject;
                }
                public static GroovyObject loadScript(String rule) {
                    if (StringUtils.isEmpty(rule)) {
                        return null;
                    }
                    try {
                        Class ruleClazz = CLASS_LOADER.parseClass(rule);
                        if (ruleClazz != null) {
                            log.info("load rule:" + rule + " success!");
                            GroovyObject groovyObject = (GroovyObject) ruleClazz.newInstance();
                            return groovyObject;
                        }
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                    } finally {
                        CLASS_LOADER.clearCache();
                    }
                    log.error("load rule error, can not load Script");
                    return null;
                }
                @Override
                public String run() {
                    // 业务逻辑执行,方便配置
                    String groovyClassLoader1 = "package script\n" +
                            "\n" +
                            "import com.example.groovy.GroovyInvokeJavaDemo\n" +
                            "import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
                            "import com.example.groovy.utils.SpringContextUtil\n" +
                            "\n" +
                            "/**\n" +
                            " * @Author: lly\n" +
                            " * @Date: 2023/7/1\n" +
                            " */\n" +
                            "\n" +
                            "def groovyInvokeJavaMethod(int a, int b) {\n" +
                            "    GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
                            "//    return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
                            "\n" +
                            "    return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)\n" +
                            "}\n" +
                            "\n" +
                            "groovyInvokeJavaMethod(a, b)";
                    String groovyClassLoader2 = "package script\n" +
                            "\n" +
                            "import com.example.groovy.GroovyInvokeJavaDemo\n" +
                            "import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
                            "import com.example.groovy.utils.SpringContextUtil\n" +
                            "\n" +
                            "/**\n" +
                            " * @Author: lly\n" +
                            " * @Date: 2023/7/1\n" +
                            " */\n" +
                            "\n" +
                            "def groovyInvokeJavaMethod(int a, int b) {\n" +
                            "    GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
                            "//    return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
                            "\n" +
                            "    return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)\n" +
                            "}\n" +
                            "\n" +
                            "groovyInvokeJavaMethod(a, b)";
                    Binding binding = new Binding();
                    binding.setVariable("a", 300);
                    binding.setVariable("b", 400);
            //        Script classLoader1 = loadScript("groovyClassLoader1", groovyClassLoader1, binding);
                    GroovyObject groovyObject = loadScript("groovyClassLoader2", groovyClassLoader2);
                    Object groovyInvokeJavaMethod = groovyObject.invokeMethod("groovyInvokeJavaMethod", new Object[]{100, 200});
                    return (String) groovyInvokeJavaMethod;
                }
            }

            第四步、创建请求API

            @RequestMapping("/groovy-class-loader/spring")
            public String groovyClassLoaderRuleTest() {
                String result = groovyClassLoaderRule.run();
                return result;
            }

            第五步、启动验证

            Groovy系列三 Java SpringBoot 整合 Groovy,第4张

            至此,咱们的Groovy系列就结束啦,代码大家需要的话可以访问我的gitHub网站获取或者留言,我私信发给大家,希望可以帮助到大家。

            https://github.com/576403061lly/groovy