概述
1.前言
因为在项目中使用了Groovy对业务能力进行一些扩展,效果比较好,所以记录分享一下,这里你可以了解:
-
为什么使用脚本语言
-
为什么选择Groovy
-
如何在项目中集成Groovy
-
Groovy的原理是什么和性能优化
-
实际使用的一些建议
2.为什么使用脚本语言
随着产品迭代、更新的速度越来越快,个性化需求也是越来越多,如:营销活动的查询与展示、商品优惠标签的透出、购物车各种优惠金额计算规则等。办法通常有如下几个方面:
-
最常见的方式是用代码枚举所有情况,即所有查询维度、所有可能的规则组合通过策略模式进行匹配
-
使用动态脚本引擎,例如Groovy,Python、JavaScript等。
引入动态脚本引擎对业务进行抽象可以满足定制化需求,大大提升项目效率。例如,现在开发的营销活动中,利用脚本引擎的动态解析执行,使用规则脚本将查询条件以及下发策略抽象出来,提升开发效率。
3.为什么选择Groovy
3.1.Groovy简介
Groovy是构建在JVM上的一个轻量级却强大的动态语言, 它结合了Python、Ruby和Smalltalk的许多强大的特性.
Groovy就是用Java写的 , Groovy语法与Java语法类似, Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码, 相对于Java, 它在编写代码的灵活性上有非常明显的提升,Groovy 可以使用其他 Java 语言编写的库.
3.2Groovy的优势
选型时需要考虑性能、稳定性、灵活性,综合考虑后选择Groovy,有如下几点原因:
-
学习曲线平缓,有丰富的语法糖,对于Java开发者非常友好;
-
技术成熟,可以无缝衔接Java代码,可以调用Java所有的库
-
动态语言,用Groovy构建DSL支持较好,部署运维方便(运行在JVM上不需要其他特殊环境);
-
优化后性能和原生JAVA接近,同时经过优化可以解决GC相关问题。
3.3.Groovy语法特性
- 结尾不需要分号
-
类的默认作用域是public, 不需要getter/setter方法
-
def关键字定义的变量类型都是Object, 任何变量, 方法都能用def定义/声明 , 在 Groovy 中 “一切都是对象 "
-
导航操作符 ( ?. )可帮助实现对象引用不为空时方法才会被调用
// java
if (object != null) {
object.getFieldA();
}
// Groovy
object?.getFieldA()
-
命令链, Groovy 可以使你省略顶级语句方法调用中参数外面的括号。“命令链”功能则将这种特性继续扩展,它可以将不需要括号的方法调用串接成链,既不需要参数周围的括号,链接的调用之间也不需要点号
def methodA(String name) {
println("A: " + name)
return this
}
def methodB(String name) {
println("B: " + name)
return this
}
def methodC() {
println("C")
return this
}
def methodD(String name) {
println("D: " + name)
return this
}
methodA("xiaoming")
methodB("zhangsan")
methodC()
methodD("lisi")
// 不带参数的链中需要用括号
methodA "xiaoming" methodB "zhangsan" methodC() methodD "lisi"
-
闭包. 闭包是一个短的匿名代码块。每个闭包会被编译成继承Groovy.lang.Closure类的类,这个类有一个叫call方法,通过该方法可以传递参数并调用这个闭包.
def hello = {println "Hello World"}
hello.call()
// 包含形式参数
def hi = {
person1, person2 -> println "hi " + person1 + ", "+ person2
}
hi.call("xiaoming", "xiaoli")
// 隐式单个参数, 'it'是Groovy中的关键字
def hh = {
println("haha, " + it)
}
hh.call("zhangsan")
-
数据结构的原生语法, 写法更便捷
def list = [11, 12, 13, 14] // 列表, 默认是ArrayList
def list = ['Angular', 'Groovy', 'Java'] as List // 字符串列表
// 同list.add(8)
list << 8
[1, 2, [3, 4], 5] // 嵌套列表
['Groovy', 21, 2.11] // 异构的对象引用列表
[] // 一个空列表
def set = ["22", "11", "22"] as Set // LinkedHashSet, as运算符转换类型
def map = ['TopicName': 'Lists', 'TopicName': 'Maps'] // map, LinkedHashMap
// 循环
map.each {
print it.key
}
-
Groovy Truth 所有类型都能转成布尔值,比如
null
,void
对象, 等同于 0 或空的值,都会解析为false
,其他则相当于true
-
Groovy支持
DSL(Domain Specific Languages领域特定语言)
, DSL旨在简化以Groovy编写的代码,使得它对于普通用户变得容易理解 -
借助命令链编写DSL
// Groovy代码
Map<String, String> result = new HashMap<>()
show = { result.put("rightGoods", it)}
square_root = { Math.sqrt(it) }
def please(action) {
[the: { what ->
[of: { n -> action(what(n)) }]
}]
}
// DSL 语言: please show the square_root of 100 (请显示100的平方根)
// 调用, 等同于:please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0
return result
-
Java 的
==
实际相当于 Groovy 的is()
方法,而 Groovy 的==
则是一个更巧妙的equals()
。 在Groovy中要想比较对象的引用,不能用==
,而应该用a.is(b)
3.4Groovy与Java的对比
-
Groovy是一门基于JVM的动态语言,同时也是一门面向对象的语言,语法上和Java非常相似。它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。Java作为一种通用、静态类型的编译型语言有很多优势,但同样存在一些负担:重新编译太费工; 静态类型不够灵活,重构起来时间可能比较长; 部署的动静太大; java的语法天然不适用生产dsl;
-
相对于Java,它在编写代码的灵活性上有非常明显的提升,对于一个长期使用Java的开发者来说,使用Groovy时能够明显地感受到负身上的“枷锁”轻了。Groovy是动态编译语言,广泛用作脚本语言和快速原型语言,主要优势之一就是它的生产力。Groovy 代码通常要比 Java 代码更容易编写,而且编写起来也更快,这使得它有足够的资格成为开发工作包中的一个附件。
-
Java不是解决动态层问题的理想语言,这些动态层问题包括原型设计、脚本处理等。可以把Groovy看作给Java静态世界补充动态能力的语言,同时Groovy已经实现了java不具备的语言特性如:函数字面值、对集合的一等支持、 对正则表达式的一等支持、 对xml的一等支持
更多参考:
-
Apache Groovy: http://www.Groovy-lang.org/syntax.html
-
Differences with Java: http://www.Groovy-lang.org/differences.html
4.Groovy的原理和使用优化
4.1Groovy原理
所有的groovy代码都运行在JVM中并且使用的是java对象模型,不管你写的是groovy类,或者是groovy脚本,它们都作为java类在JVM中运行。 在JVM中运行groovy类有两种方式:
-
使用groovyc编译所有的*.groovy为java的*.class文件,把这些*.class文件放在java类路径中,通过java类加载器来加载这些类。
-
通过groovy的类加载器在运行时直接加载*.groovy文件并且生成对象,在这种方式下,没有生成任何*.class,但是生成了一个java.lang.Class对象的实例,一个MyClass的类型将被产生并且增加到类加载器中,在代码中将像从*.class一样获取到MyClass对象。
其实这一切都要归功于 Groovy 编译器,Groovy 编译器在编译 Groovy 代码的时候,并不是像 Java 一样,直接编译成字节码,而是编译成 “动态调用的字节码”
例如下面这一段 Groovy 代码
package groovy
println("Hello World!")
当我们用Groovy编译器编译之后,就会变成
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";
}
......
}
Groovy动态方法执行原理
在Groovy中动态地注入方法、调用方法、属性就是使用元类metaClass来完成的(类似于Java的反射机制),请求的方法会被委托到这个类。
与java编译成字节码时处理方法调用不同,Groovy编译时对方法调用通用都是通过invokeMethod实现,这样提供了极强的动态方法植入能力.
所有groovy 脚本生成的class 都会实现 GroovyObject 接口,Groovy 里面所有方法调用都会通过 invokeMethod 来调用.
MetaClass and MetaClassRegistry
MetaClassRegistry 存储了所有MetaClass 包含java 的,groovy 的GroovyObject 的invokeMethod 实际是由MetaClassImpl来执行的
4.2Groovy的使用方式
Groovy在java项目中的使用方式基本上分为三种:GroovyScriptEngine、GroovyShell和GroovyClassLoader。
4.2.1 GroovyScriptEngine
GroovyScriptEngine从指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本并执行
//定义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 GroovyShell
Groovy官方提供GroovyShell,执行Groovy脚本片段,GroovyShell每一次执行时代码时会动态将代码编译成Java Class,然后生成Java对象在Java虚拟机上执行,所以如果使用GroovyShell会造成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 GroovyClassLoader
Groovy官方提供GroovyClassLoader类,支持从文件、url或字符串中动态地加载一个脚本并执行它的行为,实例化对象,反射调用指定方法。GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。
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.3Groovy使用优化
当JVM中运行的Groovy脚本存在大量并发时,如果按照默认的策略,每次运行都会重新编译脚本,调用类加载器进行类加载。不断重新编译脚本会增加JVM内存中的CodeCache和Metaspace,引发内存泄露,最后导致Metaspace内存溢出问题
什么时候会触发Metaspace的垃圾回收?
-
Metaspace在没有更多的内存空间的时候,比如加载新的类的时候;
-
JVM内部有一个叫做_capacity_until_GC的变量,一旦Metaspace使用的空间超过这个变量的值,就会对Metaspace进行回收;
-
FGC时会对Metaspace进行回收。
就算Class数量过多,只要Metaspace触发GC,那应该就不会溢出了。为什么上面会给出Metaspace溢出的结论呢?这里引出下一个问题:
JVM回收Class对象的条件是什么?
-
该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
-
加载该类的ClassLoader已经被GC;
-
java.lang.Class对象没有在任何地方被引用。
针对上述问题,增加了对脚本语言的一个缓存的优化
private final TimedCache<String, Class<?>> 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.4Groovy安全问题
Groovy会自动引入java.util,java.lang包,方便用户调用,但同时也增加了系统的风险。为了防止用户调用System.exit或Runtime等方法导致系统宕机,以及自定义的Groovy片段代码执行死循环或调用资源超时等问题,Groovy提供了SecureASTCustomizer安全管理者。
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);
}
虽然SecureASTCustomizer可以通过控制允许的代码结构来保护源代码,但是对于脚本的编写仍然存在一定的安全风险,容易造成cpu暴涨、占用磁盘空间较大等影响系统运行的问题。所以需要一些被动安全手段,比如采用线程池隔离,对脚本执行进行有效的实时监控、统计和封装等。
5.项目与Groovy的集成
5.1引入Groovy依赖
<dependency>
<groupId>org.codehaus.Groovy</groupId>
<artifactId>Groovy</artifactId>
<version>3.0.9</version>
</dependency>
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.2Groovy脚本的上下文信息
/*** Groovy脚本上下文*/
public class GroovyStandardScriptExecuteContext {
/**
* 脚本名称
*/
private String scriptClassName;
/**
* 脚本源码
*/
private String scriptClassSource;
/**
* 脚本依赖的参数 log 参数
*/
private GroovyStandardScriptSandBoxContext GroovyStandardScriptSandBoxContext;
/**
* 脚本依赖的参数 log
*/
private Map<String, Object> scriptFrameworkParam;
/**
* 依赖的feign
*/
private Map<String, Object> scriptFeignDependencyParam;
/**
* 依赖的参数 service
*/
private Map<String, Object> scriptDependencyParam;
/**
* 脚本参数
*/
private Map<String, Object> scriptParam;
/**
* 系统参数
*/
private Map<String, Object> systemParam;
/**
* 作用域的参数和变量
*/
private Binding scriptBinding;
/**
* 脚本转化的类信息
*/
private Class<?> scriptClass;
}
5.3执行Groovy的调用流程
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不可为空");
//参数map
ScriptExecuteParam 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或service
computeScriptDependencyParam(context, GroovyContext);
//设置脚本参数 log
computeScriptFrameworkParam(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");
//执行Groovy
Object 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.总结
-
Groovy是一种动态脚本语言,适用于业务变化多又快以及配置化的需求,目前项目在导购场景上,例如:营销活动信息的动态展示、购物车优惠信息的设置、商品优惠信息和标签的设置等,除此之外,Groovy也适用于一些推荐系统的内容下发,自动化构建工具Gradle和其他一些个性化场景。
-
Groovy易上手,其本质也是运行在JVM的Java代码。可以使用Groovy在提高开发效率,加快响应需求变化,为更快的交付需求提供更好的支持。
最后
以上就是自由毛衣为你收集整理的导购场景下的Groovy脚本引擎实战的全部内容,希望文章能够帮你解决导购场景下的Groovy脚本引擎实战所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复