当前位置: 首页 > news >正文

Java实现动态加载的逻辑

日常工作中我们经常遇到这样的场景,某某些逻辑特别不稳定,随时根据线上实际情况做调整,比如商品里的评分逻辑,比如规则引擎里的规则。

常见的可选方案有:

  1. JDK自带的ScriptEngine
  2. 使用groovy,如GroovyClassLoader、GroovyShell、GroovyScriptEngine
  3. 使用Spring的<lang:groovy/>
  4. 使用JavaCC实现自己的DSL

后续我们会对每一个方案做具体说明。为了方便解说,我们假定有这样一个场景,我们有一些商品对象(Product),商品上有商品ID、静态评分、相关度评分、所属类目ID,我们想要计算商品的最终得分(final_score),后续流程会基于这个评分对商品做排序。Rule是我们对评分计算逻辑的抽象,support用于提示当前Rule是否适用给定Product,execute用于对给定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 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。为了解决这个问题可以:

  1. 添加缓存,代码的MD5做为缓存KEY,GroovyClassLoader解析Class对象做为值,复用这个Class对象
  2. 促进Class和ClassLoader回收

我们知道Class回收前提是:

  1. 该Class下的对象都已经被回收
  2. 没有对当前Class的直接引用
  3. 加载当前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;}
}

XML文件配置

<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实现动态加载的逻辑

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

数据库的设计规范

文章目录 第一范式&#xff08;1NF&#xff09;&#xff1a;列不可再分 第二范式 &#xff08;2NF&#xff09;&#xff1a;所有非主键字段&#xff0c;都必须 完全依赖主键&#xff0c;不能部分依赖 第三范式&#xff08;3NF&#xff09;&#xff1a;所有非主键字段不能依赖于…...

正则表达式从放弃到入门(2):grep命令详解

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

用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的新闻网站浏览管理实现与设计 摘要&#xff1a;在大数据时代下&#xff0c;科技与技术日渐发达的时代&#xff0c;人们不再局限于只获取自己身边的信息&#xff0c;而是对全球信息获取量也日渐提高&#xff0c;网络正是打开这新世纪大门的钥匙。在传统方式下&#xff…...

【蓝桥杯软件赛 零基础备赛20周】第6周——栈

文章目录 1. 基本数据结构概述1.1 数据结构和算法的关系1.2 线性数据结构概述1.3 二叉树简介 2. 栈2.1 手写栈2.2 CSTL栈2.3 Java 栈2.4 Python栈 3 习题 1. 基本数据结构概述 很多计算机教材提到&#xff1a;程序 数据结构 算法。 “以数据结构为弓&#xff0c;以算法为箭”…...

CWE/SANS TOP 25 2022

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

Qt 天气预报项目

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

新知识-Tuple元组的使用

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

“此应用专为旧版android打造,因此可能无法运行”,问题解决方案

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

【Leetcode题单】(01 数组篇)刷题关键点总结03【数组的改变、移动】

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

Lag-Llama:基于 LlaMa 的单变量时序预测基础模型

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

vue3 :deep() 深度选择器不生效

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

从零构建属于自己的GPT系列1:数据预处理(文本数据预处理、文本数据tokenizer、逐行代码解读)

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

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常用操作、快捷键总结以及使用技巧

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

Kibana使用指南

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

wvp如果确认音频udp端口开放成功

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

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里&#xff0c;输入 python 文件路径\文件.py 就可以执行 但是有时候却报错 unexpected character after line continuation character 出现上述错误的原因是没有退出解释器&#x…...

如何用“波特三大竞争战略”为你的新产品破局?

1. 成本领先战略 (Cost Leadership)核心理念&#xff1a; 成为整个行业中成本最低的生产商或服务提供商。注意&#xff0c;成本领先不等于价格战。它的本质是通过极致的运营效率、规模经济、供应链优化或技术创新&#xff0c;把产品的底层结构性成本降到最低。这意味着&#xf…...

给STM32密码锁加个“记忆”:手把手教你用CubeMX配置I2C读写EEPROM(AT24C02)

为STM32密码锁赋予持久记忆&#xff1a;CubeMX驱动AT24C02 EEPROM全攻略 当你的密码锁在断电后依然能记住最后一次设置的密码&#xff0c;这种"记忆"能力往往能大幅提升用户体验。本文将带你深入探索如何通过I2C总线连接AT24C02 EEPROM芯片&#xff0c;为基于STM32F1…...

G-Helper:华硕笔记本轻量化控制工具全面解析与实战指南

G-Helper&#xff1a;华硕笔记本轻量化控制工具全面解析与实战指南 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目地…...

Claude Tool Use 怎么用?从零到生产的完整教程(2026)

上周接了个需求&#xff0c;做一个能查天气、查数据库、还能发邮件的 AI 助手。一开始想着用 LangChain 套一层&#xff0c;后来发现 Claude 原生的 Tool Use&#xff08;也叫 Function Calling&#xff09;已经很成熟了&#xff0c;根本不需要额外框架。但官方文档写得有点绕&…...

AI赋能无障碍:CYBER-VISION在智能导盲场景中的落地实践

AI赋能无障碍&#xff1a;CYBER-VISION在智能导盲场景中的落地实践 1. 引言&#xff1a;当科技照亮黑暗 想象一下&#xff0c;当你闭上眼睛走在繁忙的街道上&#xff0c;周围是川流不息的人群和车辆。对于全球2.85亿视障人士来说&#xff0c;这不仅是想象&#xff0c;而是每天…...

手把手教你优化SiC MOSFET模块:从铜带键合到双面散热的5个关键技术

SiC MOSFET功率模块封装优化实战&#xff1a;五大关键技术深度解析 在电力电子领域&#xff0c;碳化硅(SiC)MOSFET功率模块正逐步取代传统硅基IGBT&#xff0c;成为高效率、高功率密度应用的首选。然而&#xff0c;要充分发挥SiC材料的性能优势&#xff0c;封装技术面临前所未…...

通义千问3-VL-Reranker-8B新手教程:零基础学会混合检索排序

通义千问3-VL-Reranker-8B新手教程&#xff1a;零基础学会混合检索排序 1. 认识这个强大的多模态排序工具 想象一下&#xff0c;你正在管理一个包含文字、图片和视频的庞大数据库。当用户搜索"户外运动装备"时&#xff0c;系统返回了100个结果——有些是产品描述文…...

OpenClaw 网关重启指南:常用指令与故障修复

手把手教你一键部署OpenClaw&#xff0c;连接微信、QQ、飞书、钉钉等&#xff0c;1分钟全搞定&#xff01; 一、几种快速重启的法子 看你当初是怎么部署的&#xff0c;挑下面最适合你的那条命令就行&#xff1a; 适用情况具体命令最省事的&#xff08;系统托管模式&#xff…...

NaViL-9B多模态提示工程:图文联合prompt编写技巧与示例

NaViL-9B多模态提示工程&#xff1a;图文联合prompt编写技巧与示例 1. 多模态模型简介 NaViL-9B是一款原生支持多模态交互的大语言模型&#xff0c;能够同时处理文本和图像输入。与传统的纯文本模型不同&#xff0c;它具备视觉理解能力&#xff0c;可以分析图片内容并与用户进…...

TinyXML2性能优化终极指南:10个技巧让XML处理速度飙升

TinyXML2性能优化终极指南&#xff1a;10个技巧让XML处理速度飙升 【免费下载链接】tinyxml2 TinyXML2 is a simple, small, efficient, C XML parser that can be easily integrated into other programs. 项目地址: https://gitcode.com/gh_mirrors/ti/tinyxml2 TinyX…...