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

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文件如下:

 

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脚本代码

 

groovy

复制代码

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

2、创建测试类GroovyTest.java

 

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、运行结果

在这里插入图片描述

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

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

 

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

 

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、运行结果

在这里插入图片描述

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

1、创建SpringContextUtil.java

 

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容器中

 

java

复制代码

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

3、Groovy脚本如下

 

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、启动类代码如下

 

java

复制代码

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,运行结果如下

在这里插入图片描述

注意!!!

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

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

我最开始的用法

 

java

复制代码

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等实例的消失而消失,所以还是会有问题。

 

groovy

复制代码

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

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

 

java

复制代码

/** * 缓存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,重新装载脚本实例。

作者:summo
链接:https://juejin.cn/post/7139877924676567048
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值