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…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
HTML前端开发:JavaScript 常用事件详解
作为前端开发的核心,JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例: 1. onclick - 点击事件 当元素被单击时触发(左键点击) button.onclick function() {alert("按钮被点击了!&…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...