前言
互联网时代随着业务的飞速发展,不仅产品迭代、更新的速度越来越快,个性化需求也是越来越多。如何快速的满足各种业务的个性化需求是我们要重点思考的问题。我们开发的系统如何才能做到热部署,不重启服务就能适应各种规则变化呢?实现业务和规则的解耦 和 系统高可用性。
好了,Java的ScriptEngine脚本引擎给了我们一个选择,它支持代码动态执行,代码修改后不需要重启JVM进程,就可以使用解析或编译方式执行,非常方便,在一些动态业务规则、热更新、热修复等场景中会非常方便。
一、场景描述
在互联网项目中,我们为了引流常常会设计一些活动来吸引用户。而活动的规则呢,往往五花八门。活动和规则耦合太紧会导致系统很臃肿,难以维护,规则的变动往往需要重启服务器。我们思考是否可以将规则设计成一个黑盒子,我们传递相应的输入,期望得到相应的输出结果。
活动只需要知道规则脚本的位置,执行规则脚本而不需要知道它是如何执行的。规则脚本可以存储在数据库,磁盘文件等地方。
下面我们先介绍下各种引擎。
二、javascript语法引擎
ScriptEngineManager
为 ScriptEngine
类实现一个发现和实例化机制,还维护一个键/值对集合来存储所有 Manager 创建的引擎所共享的状态。此类使用服务提供者机制枚举所有的 ScriptEngineFactory
实现。ScriptEngineManager
提供了一个方法,可以返回一个所有工厂实现和基于语言名称、文件扩展名和 mime 类型查找工厂的实用方法所组成的数组。
键/值对的 Bindings
(即由管理器维护的 "Global Scope")对于 ScriptEngineManager
创建的所有 ScriptEngine
实例都是可用的。Bindings
中的值通常公开于所有脚本中。
JavaScriptEngine
public class JavaScriptTest {
public static void main(String[] args) throws Exception {
String js = " function add (a, b) { " +
" var sum = a + b; " +
//js调用java类
" java.lang.System.out.println(\"Script sum=\" + sum); " +
" return java.lang.Integer.valueOf(sum); " +
"}";
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(js);
Invocable jsInvoke = (Invocable) engine;
Object result = jsInvoke.invokeFunction("add", new Object[]{1, 2});
}
}
ScriptEngine (Java 2 Platform SE 6)
getEngineByName
public ScriptEngine getEngineByName(String shortName)查找并创建一个给定名称的
ScriptEngine
。该算法首先查找一个ScriptEngineFactory
,该ScriptEngineFactory
已经针对给定名称使用registerEngineName
方法注册为处理程序。
如果没有找到这样的 ScriptEngineFactory,则搜索构造方法存储的ScriptEngineFactory
实例数组,以获得具有指定名称的 ScriptEngineFactory。如果通过这两种方法之一找到了一个ScriptEngineFactory
,则用它来创建ScriptEngine
实例。参数:
shortName
-ScriptEngine
实现的短名称,由其ScriptEngineFactory
的getNames
方法返回。返回:
搜索到的工厂所创建的
ScriptEngine
。如果没有找到这样的工厂,则返回 null。ScriptEngineManager
将它自己的globalScope
Bindings
设置为新建ScriptEngine
的GLOBAL_SCOPE
Bindings
。抛出:
NullPointerException
- 如果 shortName 为 null
eval
Object eval(String script) throws ScriptException执行指定的脚本。使用
ScriptEngine
的默认ScriptContext
。参数:
script
- 要执行的脚本语言源。返回:
执行脚本所返回的值。
抛出:
ScriptException
- 如果脚本发生错误。
NullPointerException
- 如果参数为 null。
NashornScriptEngine
从 JDK 1.8 开始,Nashorn取代Rhino(JDK 1.6, JDK1.7) 成为 Java 的嵌入式 JavaScript 引擎。Nashorn 完全支持 ECMAScript 5.1 规范以及一些扩展。它使用基于 JSR 292 的新语言特性,其中包含在 JDK 7 中引入的 invokedynamic,将 JavaScript 编译成 Java 字节码。
与先前的 Rhino 实现相比,这带来了 2 到 10倍的性能提升
public static void main(String[] args) throws Exception {
String js = " function add (a, b) { " +
" var sum = a + b; " +
// js调用java类
" java.lang.System.out.println(\"Script sum=\" + sum); " +
" return java.lang.Integer.valueOf(sum); " +
"}";
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
engine.eval(js);
Invocable jsInvoke = (Invocable) engine;
Object result = jsInvoke.invokeFunction("add", new Object[]{1, 2});
}
三、Groovy语法引擎
一.使用GroovyClassLoader
用 Groovy 的 GroovyClassLoader ,它会动态地加载一个脚本并执行它。GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的Groovy类
Groovy是在Java虚拟机上实现的动态语言,提供了动态将java代码编译为Java Class对象的功能。需要添加依赖包
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.16</version>
<type>pom</type>
</dependency>
代码如下(示例1):
public class GroovyTest {
public static void main(String[] args) throws Exception {
GroovyClassLoader loader = new GroovyClassLoader();
// java代码
String java = " " +
" public class Test { " +
" public int add(double a, double b) { " +
" double sum = a + b; " +
" System.out.println(\"Script sum=\" + sum); " +
" return sum.intValue(); " +
" } " +
" } ";
Class scriptClass = loader.parseClass(java);
GroovyObject scriptInstance = (GroovyObject) scriptClass.getDeclaredConstructor().newInstance();
Object result = scriptInstance.invokeMethod("add", new Object[]{1, 2});
System.out.println("Groovy result=" + result);
}
}
代码如下(示例2):
public class GroovyTest {
public static void main(String[] args) throws Exception {
String groovy = " def call(int a,int b) { " +
" return a + b " +
"}";
GroovyClassLoader loader = new GroovyClassLoader();
Class scriptClass = loader.parseClass(groovy);
GroovyObject scriptInstance = (GroovyObject) scriptClass.newInstance();
result = scriptInstance.invokeMethod("call",new Object[] {1,2});
System.out.println("Groovy result=" + result);
}
}
如果一切都需要在1行,那么你在return语句之前就错过了一个分号。 如:
boolean engineTest = false; if (!engineTest) { engineTest = true}; return engineTest;
二、原理
GroovyClassLoader是一个定制的类装载器,在代码执行时动态加载groovy脚本为java对象。
大家都知道classloader的双亲委派,我们先来分析一下这个GroovyClassloader,看看它的祖先分别是啥:
使用idea 创建一个 Groovy项目
运行结果:
groovy.lang.GroovyClassLoader$InnerLoader@432038ec
groovy.lang.GroovyClassLoader@51891008
org.codehaus.groovy.tools.RootLoader@4d405ef7
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@ab7395e进程已结束,退出代码0
从而得出Groovy的ClassLoader体系:
Bootstrap ClassLoader ↑ sun.misc.Launcher.ExtClassLoader // 即Extension ClassLoader ↑ sun.misc.Launcher.AppClassLoader // 即System ClassLoader ↑ org.codehaus.groovy.tools.RootLoader // 以下为User Custom ClassLoader ↑ groovy.lang.GroovyClassLoader ↑ groovy.lang.GroovyClassLoader.InnerLoader
三、调用groovy脚本实现方式
1.使用GroovyClassLoader
private static void invoke(String scriptText, String function, Object... objects) throws Exception {
GroovyClassLoader classLoader = new GroovyClassLoader();
Class groovyClass = classLoader.parseClass(scriptText);
try {
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod(function,objects);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
2.使用ScriptEngine
private static final GroovyScriptEngineFactory scriptEngineFactory = new GroovyScriptEngineFactory();
private static <T> T invoke(String script, String function, Object... objects) throws Exception {
ScriptEngine scriptEngine = scriptEngineFactory.getScriptEngine();
scriptEngine.eval(script);
return (T) ((Invocable) scriptEngine).invokeFunction(function, objects);
}
3.使用GroovyShell
private static GroovyShell groovyShell = new GroovyShell();
private static <T> T invoke(String scriptText, String function, Object... objects) throws Exception {
Script script= groovyShell.parse(scriptText);
return (T) InvokerHelper.invokeMethod(script, function, objects);
}
四、性能优化
项目在测试时发现,加载的类随着程序运行越来越多,而且垃圾收集也非常频繁。
groovy脚本执行的过程
GroovyClassLoader classLoader = new GroovyClassLoader();
Class groovyClass = classLoader.parseClass(scriptText);
try {
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod(function,objects);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
查看GroovyClassLoader.parseClass方法,发现如下代码:
public Class parseClass(String text) throws CompilationFailedException {
return this.parseClass(text, "script" +
System.currentTimeMillis()
+ Math.abs(text.hashCode()) + ".groovy");
}
public Class parseClass(final String text, final String fileName) throws CompilationFailedException {
GroovyCodeSource gcs = (GroovyCodeSource)AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
public GroovyCodeSource run() {
return new GroovyCodeSource(text, fileName, "/groovy/script");
}
});
gcs.setCachable(false);
return this.parseClass(gcs);
}
protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
InnerLoader loader = (InnerLoader)AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() {
public InnerLoader run() {
return new InnerLoader(GroovyClassLoader.this);
}
});
return new ClassCollector(loader, unit, su);
}
这两处代码的意思是:
groovy每执行一次脚本,都会生成一个脚本的class对象,这个class对象的名字由 “script” + System.currentTimeMillis() +
Math.abs(text.hashCode()组成,对于问题1:每次执行同一个StrategyLogicUnit时,产生的class都不同,每次执行规则脚本都会产生一个新的class。
接着看问题2 InnerLoader部分:
groovy每执行一次脚本都会new一个InnerLoader去加载这个对象,而对于问题2,我们可以推测:InnerLoader和脚本对象都无法在fullGC的时候被回收,因此运行一段时间后将PERM占满,一直触发fullGC。
五、解决方案
把每次脚本生成的对象缓存起来
private final ConcurrentHashMap<Integer, GroovyObject> groovyMap = new ConcurrentHashMap();
private final ReentrantLock lock = new ReentrantLock();
public Object invoke(String scriptId) {
GroovyObject scriptInstance = groovyMap.get(scriptId);
if (scriptInstance == null) {
lock.lock();
try {
scriptInstance = groovyMap.get(scriptId);
if (scriptInstance == null ) {
GroovyClassLoader loader = new GroovyClassLoader();
Class scriptClass = loader.parseClass(script);
scriptInstance = (GroovyObject) scriptClass.getDeclaredConstructor().newInstance();
groovyMap.put(scriptId, scriptInstance);
}
} finally {
lock.unlock();
}
}
Object result = scriptInstance.invokeMethod("match", new Object[]{map});
return result;
}
四、项目实战
一、概述
Groovy is a multi-faceted language for the Java platform.
Apache Groovy是一种强大的、可选的类型化和动态语言,具有静态类型和静态编译功能,用于Java平台,目的在于通过简洁、熟悉和易于学习的语法提高开发人员的工作效率。它可以与任何Java程序顺利集成,并立即向您的应用程序提供强大的功能,包括脚本编写功能、特定于域的语言编写、运行时和编译时元编程以及函数式编程。
Groovy是基于java虚拟机的,执行文件可以是简单的脚本片段,也可以是一个完整的groovy class,对于java程序员来说,学习成本低,可以完全用java语法编写。
二、项目描述
我们要 设计这样一个系统,根据用户的行为,赠送用户福利。比如阅读书籍时长超过300S,我们会赠送用户书券;用户注册为正式用户,我们赠送用户积分等操作。诸如此类多条件或并的复杂场景。
三、设计Groovy模版表,存储Groovy脚本
我们先设计脚本模版表groovy
字段名称 | 字段类型 | 描述 |
---|---|---|
id | BIGINT(20) | 主键 |
groovy_code | VARCHAR(32) | Groovy模版编码 |
groovy_name | VARCHAR(32) | Groovy模版名称 |
content | TEXT | 模版内容 |
status | TINYINT(4) | 状态 1:启用 2:停用 |
update_time | DATETIME | 修改时间 |
updater | VARCHAR(32) | 最后修改人 |
version | INT(10) | 版本号 |
数据储存如图:
四、用户事件表
五、Spring Bean
不能使用@Autowired(autowired是在Spring启动后注入的,此时还未加载groovy代码,故无法注入)
建议实现ApplicationContextAware接口的工具(组件)来获取Spring Bean
调用Spring Bean的脚本文章来源:https://uudwc.com/A/woLYW
import com.xinwu.shushan.core.common.ApplicationContextHelper;
import com.xinwu.shushan.launch.infra.cache.AdClickCache;
public class Groovy {
public Boolean match(Map<String, Object> map) {
AdClickCache adClickCache = ApplicationContextHelper.getBean(AdClickCache.class);
//阈值
Integer threshold = (Integer) map.get("threshold");
//用户ID
Integer userId = (Integer) map.get("userId");
//日期
Date date = (Date) map.get("date");
//产品线
Integer productType = (Integer) map.get("productType");
//广告点击数
int adClickCount = adClickCache.get(date, productType, userId);
return adClickCount >= threshold;
}
}
六、单元测试
import com.google.common.collect.Maps;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import java.util.Date;
import java.util.Map;
public class AdClickGroovyTest {
public static void main(String[] args) throws Exception {
String script = "" +
"import com.xinwu.shushan.core.common.ApplicationContextHelper;\n" +
"import com.xinwu.shushan.launch.infra.cache.AdClickCache;\n" +
" \n" +
"public class Groovy {\n" +
" public Boolean match(Map<String, Object> map) {\n" +
" AdClickCache adClickCache = ApplicationContextHelper.getBean(AdClickCache.class);\n" +
" \n" +
" //阈值\n" +
" Integer threshold = (Integer) map.get(\"threshold\");\n" +
" //用户ID\n" +
" Integer userId = (Integer) map.get(\"userId\");\n" +
" //日期\n" +
" Date date = (Date) map.get(\"date\");\n" +
" //产品线\n" +
" Integer productType = (Integer) map.get(\"productType\");\n" +
" //广告点击数\n" +
" int adClickCount = adClickCache.get(date, productType, userId);\n" +
" \n" +
" return adClickCount >= threshold;\n" +
" }\n" +
"}";
GroovyClassLoader loader = new GroovyClassLoader();
Class scriptClass = loader.parseClass(script);
GroovyObject scriptInstance = (GroovyObject) scriptClass.newInstance();
Map<String, Object> map = Maps.newHashMap();
map.put("threshold", 2);
map.put("userId", 10001);
map.put("date", new Date());
map.put("productType", 1);
Object result = scriptInstance.invokeMethod("match", new Object[]{map});
System.out.println("Groovy result=" + result);
}
}
学习
复杂多变场景下的Groovy脚本引擎实战文章来源地址https://uudwc.com/A/woLYW