首页 >> 大全

导购场景下的Groovy脚本引擎实战

2023-09-15 大全 19 作者:考证青年

其实这一切都要归功于 编译器, 编译器在编译 代码的时候,并不是像 Java 一样,直接编译成字节码,而是编译成 “动态调用的字节码”

例如下面这一段 代码

package groovy 
println("Hello World!")

当我们用编译器编译之后,就会变成

package groovy;......public class HelloGroovy extends Script {private static /* synthetic */ ClassInfo $staticClassInfo;public static transient /* synthetic */ boolean __$stMC;private static /* synthetic */ ClassInfo $staticClassInfo$;private static /* synthetic */ SoftReference $callSiteArray;......public static void main(String ... args) {// 调用run()方法CallSite[] arrcallSite = HelloGroovy.$getCallSiteArray();arrcallSite[0].call(InvokerHelper.class, HelloGroovy.class, (Object)args);}public Object run() {CallSite[] arrcallSite = HelloGroovy.$getCallSiteArray();return arrcallSite[1].callCurrent((GroovyObject)this, (Object)"Hello World!");}......private static /* synthetic */ void $createCallSiteArray_1(String[] arrstring) {arrstring[0] = "runScript";arrstring[1] = "println";}......
}

动态方法执行原理

在中动态地注入方法、调用方法、属性就是使用元类来完成的(类似于Java的反射机制),请求的方法会被委托到这个类。

与java编译成字节码时处理方法调用不同,编译时对方法调用通用都是通过实现,这样提供了极强的动态方法植入能力.

所有 脚本生成的class 都会实现 接口, 里面所有方法调用都会通过 来调用.

and

存储了所有 包含java 的, 的 的 实际是由来执行的

4.的使用方式

在java项目中的使用方式基本上分为三种:、和。

4.2.1

从指定的位置(文件系统,URL,数据库,等等)加载脚本并执行

//定义FunGroove.groovy文件 
package com.chy.groovy
void print(){System.out.println("没有参数!!!!");
}
//执行方法
print();// GroovyScriptEngine的根路径,如果参数是字符串数组,说明有多个根路径
GroovyScriptEngine engine = new GroovyScriptEngine("src/main/java/com/chy/groovy/");Binding binding1 = new Binding();Object result1 = engine.run("FunGroove.groovy", binding1);if(null!=result1) {System.out.println(result1);}

4.2.2

官方提供,执行脚本片段,每一次执行时代码时会动态将代码编译成Java Class,然后生成Java对象在Java虚拟机上执行,所以如果使用会造成Class太多,性能较差。

脚本引擎到底是什么__脚本引擎是什么

final String script = "Runtime.getRuntime().availableProcessors()";
Binding intBinding = new Binding();
GroovyShell shell = new GroovyShell(intBinding);
final Object eval = shell.evaluate(script);
System.out.println(eval);

4.2.3

官方提供类,支持从文件、url或字符串中动态地加载一个脚本并执行它的行为,实例化对象,反射调用指定方法。是一个定制的类装载器,负责解释加载Java类中用到的类。

GroovyClassLoader groovyClassLoader = new GroovyClassLoader();String helloScript = "package com.vivo.groovy.util" +  // groovy或者Java代码"class Hello {" +"String say(String name) {" +"System.out.println(\"hello, \" + name)" +" return name;""}" +"}";
Class helloClass = groovyClassLoader.parseClass(helloScript);
GroovyObject object = (GroovyObject) helloClass.newInstance();
Object ret = object.invokeMethod("say", "world"); // 输出"hello, world"
System.out.println(ret.toString()); // 打印

4.使用优化

当JVM中运行的脚本存在大量并发时,如果按照默认的策略,每次运行都会重新编译脚本,调用类加载器进行类加载。不断重新编译脚本会增加JVM内存中的和,引发内存泄露,最后导致内存溢出问题

什么时候会触发的垃圾回收?

就算Class数量过多,只要触发GC,那应该就不会溢出了。为什么上面会给出溢出的结论呢?这里引出下一个问题:

JVM回收Class对象的条件是什么?

针对上述问题,增加了对脚本语言的一个缓存的优化

private final TimedCache> groovyScriptClassCache = CacheUtil.newTimedCache(1000L * 60 * 60 * 24);
private void compileScriptBinary(ScriptExecuteContext context, GroovyStandardScriptExecuteContext groovyContext) {Class scriptClass = groovyScriptClassCache.get(context.getScriptImplDTO().getScriptSource(), true, () -> {try (GroovyClassLoader classLoader = initializeGroovyClassLoader()) {return classLoader.parseClass(context.getScriptImplDTO().getScriptSource());}});Assert.that(Objects.nonNull(scriptClass), "ScriptClass不能为空");groovyContext.setScriptClass(scriptClass);
}

4.安全问题

会自动引入java.util,java.lang包,方便用户调用,但同时也增加了系统的风险。为了防止用户调用.exit或等方法导致系统宕机,以及自定义的片段代码执行死循环或调用资源超时等问题,提供了安全管理者。

final SecureASTCustomizer groovyStandardSecureASTCustomizer;
public GroovyClassLoader initializeGroovyClassLoader() {CompilerConfiguration compilerConfiguration = new CompilerConfiguration();//自定义CompilerConfiguration,设置groovy 编译选项,比如设置基类,设置默认导包,安全校验AST等等等,其他自定义操作compilerConfiguration.addCompilationCustomizers(groovyStandardSecureASTCustomizer);ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();return new GroovyClassLoader(contextClassLoader, compilerConfiguration);
}

虽然可以通过控制允许的代码结构来保护源代码,但是对于脚本的编写仍然存在一定的安全风险,容易造成cpu暴涨、占用磁盘空间较大等影响系统运行的问题。所以需要一些被动安全手段,比如采用线程池隔离,对脚本执行进行有效的实时监控、统计和封装等。

5.项目与的集成 5.1引入依赖

org.codehaus.GroovyGroovy3.0.9

5.2封装脚本框架所需参数

5.2.1脚本框架上下文信息

/*** 脚本框架上下文*/
public class ScriptExecuteContext {/*** 脚本定义code*/private String scriptCode;/*** 脚本版本号*/private Integer version;/*** 脚本定义*/private ScriptDTO scriptDTO;/*** 脚本实现*/private ScriptImplDTO scriptImplDTO;/*** 脚本扩展定义*/private ScriptFeatureDTO scriptFeatureDTO;/*** 脚本扩展实现*/private ScriptImplFeatureDTO scriptImplFeatureDTO;/*** 脚本预约执行器*/private ScriptFramework scriptFramework;/*** 脚本执行参数*/private ScriptExecuteParam scriptExecuteParam;/*** 脚本执行结果*/private ScriptExecuteResult scriptExecuteResult;
}

脚本引擎到底是什么_脚本引擎是什么_

5.2.脚本的上下文信息

/*** Groovy脚本上下文*/
public class GroovyStandardScriptExecuteContext {/*** 脚本名称*/private String scriptClassName;/*** 脚本源码*/private String scriptClassSource;/*** 脚本依赖的参数 log 参数*/private GroovyStandardScriptSandBoxContext GroovyStandardScriptSandBoxContext;/*** 脚本依赖的参数 log*/private Map scriptFrameworkParam;/*** 依赖的feign*/private Map scriptFeignDependencyParam;/*** 依赖的参数 service*/private Map scriptDependencyParam;/*** 脚本参数*/private Map scriptParam;/*** 系统参数*/private Map systemParam;/*** 作用域的参数和变量*/private Binding scriptBinding;/*** 脚本转化的类信息*/private Class scriptClass;
}

5.3执行的调用流程

5.1.1.请求对应接口

    @PostMapping("execute")@ApiOperation(value = "小程序-脚本引擎调用")@ApiMonitor(name = "小程序-脚本引擎调用")public Result execute(@RequestBody ScriptExecuteRequest request) {StopWatch stopWatch = StopWatch.createStarted();Date requestTime = new Date();Assert.notBlank(request.getScriptCode(), "ScriptCode不可为空");//参数mapScriptExecuteParam scriptExecuteParam = new ScriptExecuteParam();scriptExecuteParam.setParamMap(request.getScriptParam());BaseContextHandler.set(CommonConstants.CONTEXT_SELLER_ID, request.getSellerId());ScriptExecuteContext scriptExecuteContext = new ScriptExecuteContext();scriptExecuteContext.setScriptCode(request.getScriptCode());scriptExecuteContext.setScriptExecuteParam(scriptExecuteParam);//执行脚本scriptExecuteTemplate.execute(scriptExecuteContext);Object result = Optional.ofNullable(scriptExecuteContext.getScriptExecuteResult()).map(ScriptExecuteResult::getResult).orElse(null);SlsEntity slsEntity = SlsEntity.builder().put("scriptCode", request.getScriptCode()).put("businessStatus", "SUCCESS").put("requestTime", DateUtil.formatDateTime(requestTime)).put("consumingTime", String.valueOf(stopWatch.getTime(TimeUnit.MILLISECONDS)));SLog.info(slsEntity,"{} | {} ",request.getScriptCode(), "SUCCESS");return Result.success(result);}

5.1.2.执行脚本语言

public void execute(ScriptExecuteContext context){//加载脚本代码loadScript(context);//设置脚本代码及实现loadScriptFeature(context);loadScriptImplFeature(context);//加载脚本框架对应的脚本语言的实现loadScriptFramework(context);//执行脚本executeScript(context);
}

5.1.2.1加载脚本代码

public void loadScript(ScriptExecuteContext context) {try {ScriptCache scriptCache = SCRIPT_CACHE.get(context.getScriptCode());cn.hutool.core.lang.Assert.notNull(scriptCache, "脚本代码不存在");if (ObjectUtil.isNull(context.getVersion())) {// 正式版本代码context.setScriptDTO(scriptCache.getScriptDTO());context.setScriptImplDTO(scriptCache.getScriptImplDTO());} else {// 指定版本代码context.setScriptDTO(scriptCache.getScriptDTO());context.setScriptImplDTO(getScriptImplByScriptIdAndVersion(scriptCache.getScriptDTO().getId(), context.getVersion()));}} catch (Exception e) {log.error("get script error params = {} ", JSON.toJSONString(context), e);cn.hutool.core.lang.Assert.isTrue(false, "执行脚本获取失败");}
}

5.1.2.2设置脚本代码及实现

public void loadScriptFeature(ScriptExecuteContext context) {ScriptDTO scriptDTO = context.getScriptDTO();context.setScriptFeatureDTO(scriptDTO.getFeatures());
}public void loadScriptImplFeature(ScriptExecuteContext context) {ScriptImplDTO scriptImplDTO = context.getScriptImplDTO();context.setScriptImplFeatureDTO(scriptImplDTO.getFeatures());
}

5.1.2.3加载脚本框架对应的脚本语言的实现

public void loadScriptFramework(ScriptExecuteContext context) {ScriptImplDTO scriptImplDTO = context.getScriptImplDTO();Integer scriptFrameworkId = scriptImplDTO.getScriptFrameworkId();ScriptFramework scriptFramework = scriptFrameworkFactory.getInstance(scriptFrameworkId);Assert.that(Objects.nonNull(scriptFramework), "脚本框架不存在");//目前只有Groovy的实现context.setScriptFramework(scriptFramework);
}

5.1.2.4使用对应脚本语言执行脚本代码

public void executeScript(ScriptExecuteContext context) throws ExecutionException, InterruptedException, TimeoutException {//设置Groovy脚本的上下文GroovyStandardScriptExecuteContext GroovyContext = new GroovyStandardScriptExecuteContext();//设置上下文的执行脚本computeClassSource(context, GroovyContext);//设置上下文执行脚本的类名computeClassName(context, GroovyContext);//加载执行脚本并转化成类信息compileScriptBinary(context, GroovyContext);//设置系统参数(header)computeSystemParam(context, GroovyContext);//设置请求参数(param)computeScriptParam(context, GroovyContext);//设置依赖属性 feign或servicecomputeScriptDependencyParam(context, GroovyContext);//设置脚本参数 logcomputeScriptFrameworkParam(context, GroovyContext);//绑定作用域参数和变量computeScriptBinding(context, GroovyContext);//执行脚本并返回结果executeScriptBinary(context, GroovyContext);
}

private void executeScriptBinary(ScriptExecuteContext context, GroovyStandardScriptExecuteContext GroovyContext) {Runnable scriptRunTask = instanceScriptRunTask(context, GroovyContext);scriptRunTask.run();
}private Runnable instanceScriptRunTask(ScriptExecuteContext context, GroovyStandardScriptExecuteContext GroovyContext) {return () -> {try {//获取转化后的Groovy文件Class scriptClass = GroovyContext.getScriptClass();//获取脚本作用域的参数和变量Binding scriptBinding = GroovyContext.getScriptBinding();//获取Groovy的构建类Constructor constructor = scriptClass.getConstructor(Binding.class);//初始化Groovy类实例Object scriptClassInstance = constructor.newInstance(scriptBinding);//获取Groovy中的run方法Method run = scriptClass.getDeclaredMethod("run");//执行GroovyObject result = run.invoke(scriptClassInstance);//设置返回结果ScriptExecuteResult scriptExecuteResult = new ScriptExecuteResult(result);context.setScriptExecuteResult(scriptExecuteResult);} catch (Exception e) {if (e instanceof InvocationTargetException && ((InvocationTargetException) e).getTargetException() instanceof IllegalArgumentException) {throw new BaseException(((InvocationTargetException) e).getTargetException().getMessage());} else {log.error(LogUtil.exceptionMessage("scriptRunTaskException", e));throw new BaseException("脚本执行时发生异常");}} finally {log.info(LogUtil.message("GroovyStandardScriptSandBoxContext", GroovyContext.getGroovyStandardScriptSandBoxContext()));}};
}

5.1.3封装返回结果

Object result = Optional.ofNullable(scriptExecuteContext.getScriptExecuteResult()).map(ScriptExecuteResult::getResult).orElse(null);
SlsEntity slsEntity = SlsEntity.builder().put("scriptCode", request.getScriptCode()).put("businessStatus", "SUCCESS").put("requestTime", DateUtil.formatDateTime(requestTime)).put("consumingTime", String.valueOf(stopWatch.getTime(TimeUnit.MILLISECONDS)));
SLog.info(slsEntity,"{} | {} ",request.getScriptCode(), "SUCCESS"

6.总结

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了