深圳做网站600,3d动画制作软件下载,平面设计培训价格,wordpress 喜欢分享插件日常工作中我们经常遇到这样的场景#xff0c;某某些逻辑特别不稳定#xff0c;随时根据线上实际情况做调整#xff0c;比如商品里的评分逻辑#xff0c;比如规则引擎里的规则。
常见的可选方案有: JDK自带的ScriptEngine 使用groovy#xff0c;如GroovyClassLoader、Gro…日常工作中我们经常遇到这样的场景某某些逻辑特别不稳定随时根据线上实际情况做调整比如商品里的评分逻辑比如规则引擎里的规则。
常见的可选方案有: JDK自带的ScriptEngine 使用groovy如GroovyClassLoader、GroovyShell、GroovyScriptEngine 使用Spring的lang:groovy/ 使用JavaCC实现自己的DSL
后续我们会对每一个方案做具体说明。为了方便解说我们假定有这样一个场景我们有一些商品对象(Product)商品上有商品ID、静态评分、相关度评分、所属类目ID我们想要计算商品的最终得分(final_score)后续流程会基于这个评分对商品做排序。Rule是我们对评分计算逻辑的抽象support用于提示当前Rule是否适用给定Productexecute用于对给定Product做处理。RuleEngine负责维护一组Rule对象当调用apply时用所有Rule对给定Product做处理。 这3个文件的源码分别如下Product类 package com.lws.rule;import lombok.Data;Data
public class Product {private long id;private float staticScore;private float relationScore;private float finalScore;private int categoryId;
}Rule接口
package com.lws.rule;public interface Rule {public boolean support(Product p);public Product execute(Product p);
}
RuleEngine实现
package com.lws.rule;import java.util.ArrayList;
import java.util.List;public class RuleEngine {private ListRule rules new ArrayList();public Product apply(Product p) {for (Rule rule : rules) {if (p ! null rule.support(p)) {p rule.execute(p);}}return p;}
} 1.ScriptEngine
1.1 前景提要
JDK自带ScriptEngine实现JDK15之后默认ECMAScript引擎实现已经从JDK里移除使用前需要自己引入nashorn-core的依赖
dependencygroupIdorg.openjdk.nashorn/groupIdartifactIdnashorn-core/artifactIdversion15.4/version
/dependency
通过引入依赖自动添加ScriptEngine的实现采用的是Java SPI的机制关于Java SPI的更多信息查看文章Java SPI。通过ScriptEngineManager的代码能确定具体实现 1.2 具体实现
我们将通过ScriptEngine执行脚本的逻辑封装到一个方法内部将一个Map对象绑定到Bindings上做为执行上下文
private Object eval(String expr, MapString, Object context) {try {ScriptEngineManager manager new ScriptEngineManager();ScriptEngine engine manager.getEngineByName(JavaScript);Bindings bindings engine.createBindings();bindings.putAll(context);return engine.eval(expr, bindings);} catch (Exception e) {log.error(fail to execute expression: expr, e);return null;}
}
新建一个类JavaScriptEngineRule做为Rule的实现类support和execute都通过执行脚本返回的结果做为输出而这两个脚本是可配置的甚至可以从数据库、配置中心里读取
public class JavaScriptEngineRule implements Rule {private Logger log LoggerFactory.getLogger(JavaScriptEngineRule.class);private String supportExpr;private String executeExpr;public JavaScriptEngineRule(String supportExpr, String executeExpr) {this.supportExpr supportExpr;this.executeExpr executeExpr;}Overridepublic boolean support(Product p) {if (StringUtils.isBlank(supportExpr)) {return true;} else {Boolean b (Boolean) eval(supportExpr, Maps.of(product, p));return b ! null b;}}Overridepublic Product execute(Product p) {Product np (Product) eval(executeExpr, Maps.of(product, p));return np;}private Object eval(String expr, MapString, Object context);
}
1.3 测试结果
我们预先定义了一条数据
Product p new Product();
p.setId(1);
p.setCategoryId(1001);
p.setStaticScore(1F);
p.setRelationScore(3F);
定义执行的脚本可以看到我们只处理id是基数categoryId大于1000的Product将finalScore修改为staticScore、relationScore按比例加层后总分。一段脚本代码里可以有多个语句最后一条语句的执行结果做为ScriptEngine.eval的执行结果返回。
String supportExpr product.id % 2 1 product.categoryId 1000;
String executeExpr product.finalScore product.staticScore * 0.6 product.relationScore * 0.4; product;实际测试代码后续的测试都会重复使用预定义的数据和执行输出但不会再反复贴出
Rule rule new JavaScriptEngineRule(supportExpr, executeExpr);
if (rule.support(p)) {p rule.execute(p);
}
System.out.println(p);
2. 使用Groovy能力
通过JavaScript的ScriptEngine使用动态逻辑用起来还算简单但是也有一个明显的问题JavaScript引擎没法调用工程内的Java类库如果我想要在动态逻辑里发生HTTP请求、使用JDBC、发生MQ消息等等就很难做到。而Groovy能帮助我们达成这些目标。
2.1 GroovyClassLoader
将完整的Rule实现存储到字符串中(数据库、配置中心)由GroovyClassLoader解析生成Class再通过反射创建实例。我们创建的Rule实现类名字是GroovyClassLoaderRule他会将所有调用委托给通过反射创建的实例。
public class GroovyClassLoaderRule implements Rule {private String subClass package com.lws.rule.impl; import com.lws.rule.Product;import com.lws.rule.Rule; public class TemporaryGroovySubClass implements Rule { Overridepublic boolean support(Product p) {return p.getId() % 2 1 p.getCategoryId() 1000;} Overridepublic Product execute(Product p) {double score p.getStaticScore() * 0.6 p.getRelationScore() * 0.4;p.setFinalScore((float)score);return p;}};private Rule instance;public void init() throws InstantiationException, IllegalAccessException {GroovyClassLoader classLoader new GroovyClassLoader();Class clazz classLoader.parseClass(subClass);instance (Rule)clazz.newInstance();}Overridepublic boolean support(Product p) {return instance.support(p);}Overridepublic Product execute(Product p) {return instance.execute(p);}
}
可以看到subClass字符串里已经是正常的Java代码了Java1.7的代码基本都能正常编译。通过调用init方法我们创建了Rule的实例。这里由一个比较容易成为陷阱的问题是使用完全相同的subClass内容创建两个GroovyClassLoaderRule实例时实际创建的是两个ClassLoader实例存在完全不同的两个Class对象会占用两份JVM永久代空间
GroovyClassLoaderRule rule new GroovyClassLoaderRule();
rule.init();GroovyClassLoaderRule rule1 new GroovyClassLoaderRule();
rule1.init();System.out.println(rule.getInstance().getClass().getName()); // 这里输出的名字完全相同
System.out.println(rule1.getInstance().getClass().getName());System.out.println(rule.getInstance().getClass() rule1.getInstance().getClass()); // 但Class对象却不是一个
问题根本的原因是同一个ClassLoader同一个类只能加载一次要反复加载同一个类名就需要使用不同的ClassLoader。为了解决这个问题可以: 添加缓存代码的MD5做为缓存KEYGroovyClassLoader解析Class对象做为值复用这个Class对象 促进Class和ClassLoader回收
我们知道Class回收前提是: 该Class下的对象都已经被回收 没有对当前Class的直接引用 加载当前Class的ClassLoader没有直接引用 2.2 GroovyShell
GroovyClassLoader通过动态的源码直接创建了一个Class对象有时候我们的动态逻辑并没有那么复杂。GroovyShell的使用方式更像ScriptEngine可以指定一段脚本直接返回计算结果。
如果是直接执行脚本来获取结果GroovyShell的实现和之前的JavaScriptEngineRule基本一致执行修改eval方法的实现
private Object eval(String expr, Product product) {Binding binds new Binding();binds.setVariable(product, product);GroovyShell shell new GroovyShell(binds);Script script shell.parse(expr);return script.run();
}
这段代码里的先执行shell.parse再执行script.run可以用evaluate方法直接代码evaluate方法内部实际调用的parse、run方法
private Object eval(String expr, Product product) {Binding binds new Binding();binds.setVariable(product, product);GroovyShell shell new GroovyShell(binds);return shell.evaluate(expr);
}测试脚本可以用JavaScriptEngineRule的脚本也可以自己稍作修改在返回值前在return关键字
String supportExpr product.id % 2 1 product.categoryId 1000;
String executeExpr product.finalScore product.staticScore * 0.6 product.relationScore * 0.3; product;
GroovyShellRule rule new GroovyShellRule(supportExpr, executeExpr);
除了直接调用脚本之外GroovyShell还允许我们定义和调用函数比如我们将上面的executeExpr逻辑通过一个函数实现的话
private String functions def support(p) {return p.id % 2 1 p.categoryId 1000}def execute(p) {p.finalScore p.staticScore * 0.6 p.relationScore * 0.3; return p;};
private Object eval(String method, Product product) {GroovyShell shell new GroovyShell();Script script shell.parse(functions);return script.invokeMethod(method, product);
}
2.3 GroovyScriptEngine
GroovyScriptEngine和GroovyClassLoader类似不同的是GroovyScriptEngine指定根目录通过文件名自动加载根目录下的文件创建了instance实例之后逻辑和GroovyClassLoader的实现就完全相同了。
public void init() throws Exception {GroovyScriptEngine engine new GroovyScriptEngine(src/main/java/groovy);ClassTemporaryGroovySubClass clazz engine.loadScriptByName(TemporaryGroovySubClass.java);instance clazz.newInstance();
}
3. Spring的lang:groovy
当今主流的Java应用尤其是Web端应用基本都托管在Spring容器下如果代码由变更的情况下Bean实例的逻辑自动变更的话还是很方便的。我定义几个最简单的类
public interface ProductFactory {public Product getProduct();
}
我们期望动态加载的实现测试过程中我会修改id字段的值来查看Bean是否重新加载
public class ProductFactoryImpl implements ProductFactory{public Product getProduct() {Product p new Product();p.setId(1L);return p;}
}
XML文件配置
beans xmlnshttp://www.springframework.org/schema/beansxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexmlns:langhttp://www.springframework.org/schema/langxsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.5.xsdlang:groovy idfactory refresh-check-delay5000 script-sourcefile:D:/Workspace/groovy/ProductFactoryImpl.java//beans
测试代码
public class SpringMain {public static void main(String[] args) throws InterruptedException {ClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(application.xml);ProductFactory factory (ProductFactory) context.getBean(factory);while (true) {Thread.sleep(1000);System.out.println(factory.getProduct());}}
}
3.1 实现原理
lang:groovy/生成的Bean是Spring提供的代理Bean通过AOP生成代理对象代理对象下面包含实际的数据对象通过刷新这个数据对象让Bean表现的像是自动更新。
3.2 无法转型
一开始我没有为ProductFactoryImpl定义接口在Java的main方法里直接引用了ProductFactoryImpl类(因为他也在ClassPath下)这回导致Java的类加载器加载这个Class对象。lang:groovy/运行时再次加载ProductFactoryImpl成为一个新的Class对象。而这两个Class对象分属于不同的类加载相互之间无法转换也无法赋值。
同样是因为一开始没有定义接口导致lang:groovy/设置必须使用类代理proxy-target-classtrue配置最终导致如下报错 究其原因是在AOP调用的时候通过method实例反射调用而执行过程中却发现这个method不是target对象里的method。具体证据如下: target上的getProduct方法和invokeJoinpointUsingReflection的method方法已经不是同一个实例。
总的来说要想正确的使用lang:groovy/需要注意两点为script-source执行的对象设计接口不用指定proxy-target-class。通过日志可以看到product.id的修改是生效的。 4. JavaCC自定义DSL
JavaCC定义自己的DSL提供了更多的灵活性也会大大的增加成本自己定义的DSL可能会有潜在的问题后续我们会专门出一篇JavaCC的文章敬请期待。
5. 我该如何选择
如果只支持简单的逻辑ScriptEngine够用的情况下直接用ScriptEngine即可。对动态脚本的能力要求较高时选择Groovy的方案要注意Class的回收。lang:groovy/做成通过数据库/配置中心加载动态代码的改造相对较大如果不介意依然依赖文件系统特定位置的文件的话也不失为一种选择。