Java实现动态加载的逻辑
日常工作中我们经常遇到这样的场景,某某些逻辑特别不稳定,随时根据线上实际情况做调整,比如商品里的评分逻辑,比如规则引擎里的规则。
- JDK自带的ScriptEngine
- 使用groovy,如GroovyClassLoader、GroovyShell、GroovyScriptEngine
- 使用Spring的<lang:groovy/>
- 使用JavaCC实现自己的DSL
后续我们会对每一个方案做具体说明。为了方便解说,我们假定有这样一个场景,我们有一些商品对象(Product),商品上有商品ID、静态评分、相关度评分、所属类目ID,我们想要计算商品的最终得分(final_score),后续流程会基于这个评分对商品做排序。Rule是我们对评分计算逻辑的抽象,support用于提示当前Rule是否适用给定Product,execute用于对给定Product做处理。RuleEngine负责维护一组Rule对象,当调用apply时,用所有Rule对给定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;
}
package com.lws.rule;public interface Rule {public boolean support(Product p);public Product execute(Product p);
}
package com.lws.rule;import java.util.ArrayList;
import java.util.List;public class RuleEngine {private List<Rule> 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的依赖
<dependency><groupId>org.openjdk.nashorn</groupId><artifactId>nashorn-core</artifactId><version>15.4</version>
</dependency>
通过引入依赖自动添加ScriptEngine的实现,采用的是Java SPI的机制,关于Java SPI的更多信息查看文章Java SPI。通过ScriptEngineManager的代码能确定具体实现
1.2 具体实现
我们将通过ScriptEngine执行脚本的逻辑封装到一个方法内部,将一个Map对象绑定到Bindings上做为执行上下文
private Object eval(String expr, Map<String, 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, Map<String, 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做为缓存KEY,GroovyClassLoader解析Class对象做为值,复用这个Class对象
- 促进Class和ClassLoader回收
- 该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");Class<TemporaryGroovySubClass> 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;}
}
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:lang="http://www.springframework.org/schema/lang"xsi:schemaLocation="http://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.xsd"><lang:groovy id="factory" refresh-check-delay="5000" script-source="file: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-class="true"配置,最终导致如下报错
究其原因是在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/>做成通过数据库/配置中心加载动态代码的改造相对较大,如果不介意依然依赖文件系统特定位置的文件的话,也不失为一种选择。
相关文章:

Java实现动态加载的逻辑
日常工作中我们经常遇到这样的场景,某某些逻辑特别不稳定,随时根据线上实际情况做调整,比如商品里的评分逻辑,比如规则引擎里的规则。 常见的可选方案有: JDK自带的ScriptEngine 使用groovy,如GroovyClassLoader、Gro…...

数据库的设计规范
文章目录 第一范式(1NF):列不可再分 第二范式 (2NF):所有非主键字段,都必须 完全依赖主键,不能部分依赖 第三范式(3NF):所有非主键字段不能依赖于…...

正则表达式从放弃到入门(2):grep命令详解
正则表达式从放弃到入门(2):grep命令详解 总结 本博文转载自 这是一篇”正则表达式”扫盲贴,如果你还不理解什么是正则表达式,看这篇文章就对了。 如果你是一个新手,请从头阅读这篇文章,如果你…...

用Java写一个王者荣耀游戏
目录 sxt包 Background Bullet Champion ChampionDaji GameFrame GameObject Minion MinionBlue MinionRed Turret TurretBlue TurretRed beast包 Bear Beast Bird BlueBuff RedBuff Wolf Xiyi 打开Eclipse创建图片中的几个包 sxt包 Background package sxt;…...

基于SSM的新闻网站浏览管理实现与设计
基于ssm的新闻网站浏览管理实现与设计 摘要:在大数据时代下,科技与技术日渐发达的时代,人们不再局限于只获取自己身边的信息,而是对全球信息获取量也日渐提高,网络正是打开这新世纪大门的钥匙。在传统方式下ÿ…...

【蓝桥杯软件赛 零基础备赛20周】第6周——栈
文章目录 1. 基本数据结构概述1.1 数据结构和算法的关系1.2 线性数据结构概述1.3 二叉树简介 2. 栈2.1 手写栈2.2 CSTL栈2.3 Java 栈2.4 Python栈 3 习题 1. 基本数据结构概述 很多计算机教材提到:程序 数据结构 算法。 “以数据结构为弓,以算法为箭”…...

CWE/SANS TOP 25 2022
我整理了CWE/SANS TOP25 2022年的这25类缺陷,分类适合的开发语言,其实主要是C/C语言的缺陷相对于Java、PHP、Python、C#等更高级的语言的不同,所以分为适合C/C语言和其它语言。但是大家不要纠结,例如SQL难道C/C语言程序没有吗&…...

Qt 天气预报项目
参考引用 QT开发专题-天气预报 1. JSON 数据格式 1.1 什么是 JSON JSON (JavaScript Object Notation),中文名 JS 对象表示法,因为它和 JS 中对象的写法很类似 通常说的 JSON,其实就是 JSON 字符串,本质上是一种特殊格式的字符串…...

新知识-Tuple元组的使用
文章目录 前言一、tuple元组是什么?二、解决方法总结 前言 这次碰到一个需求,大致需要把表A中的字段1和字段2作为共同的表去查表B,并且一次性需要查多条,一开始是想的是根据字段1和字段2去查然后循环多次,但是这样反复…...

“此应用专为旧版android打造,因此可能无法运行”,问题解决方案
当用户在Android P系统上打开某些应用程序时,可能会弹出一个对话框,提示内容为:“此应用专为旧版Android打造,可能无法正常运行。请尝试检查更新或与开发者联系”。 随着Android平台的发展,每个新版本通常都会引入新的…...

【Leetcode题单】(01 数组篇)刷题关键点总结03【数组的改变、移动】
【Leetcode题单】(01 数组篇)刷题关键点总结03【数组的改变、移动】(3题) 数组的改变、移动453. 最小操作次数使数组元素相等 Medium665. 非递减数列 Medium283. 移动零 Easy 大家好,这里是新开的LeetCode刷题系列&…...

Lag-Llama:基于 LlaMa 的单变量时序预测基础模型
文章构建了一个通用单变量概率时间预测模型 Lag-Llama,在来自Monash Time Series库中的大量时序数据上进行了训练,并表现出良好的零样本预测能力。在介绍Lag-Llama之前,这里简单说明什么是概率时间预测模型。概率预测问题是指基于历史窗口内的…...

vue3 :deep() 深度选择器不生效
vue3 :deep() 深度选择器不生效 问题出在根节点上,如果没有这个根节点,那么:deep()不起作用,我把根节点加上,:deep()样式就生效了。在组件外加个 就生效了 参考: 添加链接描述...

从零构建属于自己的GPT系列1:数据预处理(文本数据预处理、文本数据tokenizer、逐行代码解读)
🚩🚩🚩Hugging Face 实战系列 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在PyCharm中进行 本篇文章配套的代码资源已经上传 从零构建属于自己的GPT系列1:文本数据预处理 从零构建属于自己的GPT系列2:语…...

c++中函数的引用
函数中的引用 引用可以作为函数的形参 不能返回局部变量的引用 #include<iostream> #include<stdlib.h> using namespace std; //形参是引用 void swap(int *x, int *y)//*x *y表示对x y取地址 { int tmp *x; *x *y; *y tmp; } void test01() { …...

IDA常用操作、快捷键总结以及使用技巧
先贴一张官方的图,然后我再总结一下,用的频率比较高的会做一些简单标注 快捷键 F系列【主要是调试状态的处理】 F2 添加/删除断点F4 运行到光标所在位置F5 反汇编F7 单步步入F8 单步跳过F9 持续运行直到输入/断点/结束 shift系列【主要是调出对应的页…...

Kibana使用指南
使用介绍主要特点应用场景数据可视化还有哪些类型安装步骤安装配置参数Elasticsearch配置参数注意事项 使用介绍 Kibana是一个开源的分析与可视化平台,设计出来用于和Elasticsearch一起使用的。可以用Kibana搜索、查看、交互存放在Elasticsearch索引里的数据&#…...

wvp如果确认音频udp端口开放成功
用到工具 在服务器上开启端口监听 选中udp server,点击创建按钮 设置服务器监听端口 在客户端连接服务器端口 选中udp客户端,点击创建 输入服务器地址 远程端口和本地端口,本地端口只要没被占用都可以使用 ,点击确认 发送数据 …...

C#文件夹基本操作(判断文件夹是否存在、创建文件夹、移动文件夹、删除文件夹以及遍历文件夹中的文件)
目录 一、判断文件夹是否存在 1.Directory类的Exists()方法 2. DirectoryInfo类的Exists属性 二、创建文件夹 1. Directory类的CreateDirectory()方法 2.DirectoryInfo类的Create()方法 三、移动文件夹 1. Directory类的Move()方法 2.DirectoryInfo类的MoveT…...

python 交互模式和命令行模式的问题
python 模式的冲突 unexpected character after line continuation character 理论上 ide里,输入 python 文件路径\文件.py 就可以执行 但是有时候却报错 unexpected character after line continuation character 出现上述错误的原因是没有退出解释器&#x…...

计算机网络——数据链路层
目录 一、数据链路层的基本概念 (一)数据链路层的概念 (二)帧 (三)数据链路层分为哪两个部分 (1)LLC(逻辑控制访问) (2)MAC&…...

【限时免费】20天拿下华为OD笔试之【哈希集合】2023B-明明的随机数【欧弟算法】全网注释最详细分类最全的华为OD真题题解
文章目录 题目描述与示例题目描述输入描述输出描述:示例 1输入输出说明 解题思路代码PythonJavaC时空复杂度 华为OD算法/大厂面试高频题算法练习冲刺训练 题目描述与示例 题目描述 明明生成了N 个 1 至 500 之间的随机整数。请你删去其中重复的数字,即…...

播放器开发(五):视频帧处理并用SDL渲染播放
目录 学习课题:逐步构建开发播放器【QT5 FFmpeg6 SDL2】 步骤 VideoOutPut模块 1、初始化【分配缓存、读取信息】 2、开始线程工作【从队列读帧->缩放->发送渲染信号到窗口】 VideoWidget自定义Widget类 1、定义内部变量 2、如果使用SDL,需要进…...

Spring MVC数据绑定的几种方法(一)
这篇文章包含spring mvc的默认数据类型绑定和简单数据类型绑定。内容来自实验。 准备: (1)在IDEA环境中从archetye创建webapp类型的maven项目exp6。 (2)在src\main目录下创建并标注java源代码文件夹和resources资源文…...

CSP-坐标变换(其二)
问题描述 对于平面直角坐标系上的坐标 (x,y),小 P 定义了如下两种操作: 拉伸 k 倍:横坐标 x 变为 kx,纵坐标 y 变为 ky; 旋转 θ:将坐标 (x,y) 绕坐标原点 (0,0) 逆时针旋转 θ 弧度(0≤θ<…...

docker 安装jekins
echo Asia/Shanghai >/etc/timezone,容器中操作报错:docker容器中 Permission denied 使用该-u选项时,可以使用root用户(ID 0),而不是用默认用户登录docker容器 docker exec -u 0 -it f8a2b3d91455 /bin/bash 或者ÿ…...

ChatGPT 问世一周年之际,开源大模型能否迎头赶上?
就在11月30日,ChatGPT 迎来了它的问世一周年,这个来自 OpenAI 的强大AI在过去一年里取得了巨大的发展,迅速吸引各个领域的用户群体。 我们首先回忆一下 OpenAI和ChatGPT这一年的大事记(表格由ChatGPT辅助生成)&#x…...

数据结构和算法-哈夫曼树以相关代码实现
文章目录 总览带权路径长度哈夫曼树的定义哈夫曼树的构造法1法2 哈夫曼编码英文字母频次总结实验内容: 哈夫曼树一、上机实验的问题和要求(需求分析):二、程序设计的基本思想,原理和算法描述:三、调试和运行…...

Kafka 的起源和背景
Apache Kafka 是一个分布式流处理平台,被广泛用于构建实时数据流应用程序和大数据处理系统。本文将深入探讨 Kafka 的起源、设计原则以及它在大数据领域中的重要作用。 大数据和实时数据处理背景 在大数据时代,处理海量数据和实时数据成为了一项关键挑…...

三极管在数字电路中的应用
一、认识三极管 三极管拥有3个引脚,分别对应3个级:基极(Base)、发射极(Emitter)、集电极(Collector),如下图所示;下图横向左侧的是基极,带箭头的那个引脚就是发射极,另一个就是集电…...