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/>做成通过数据库/配置中心加载动态代码的改造相对较大,如果不介意依然依赖文件系统特定位置的文件的话,也不失为一种选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/183873.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MacBook macOs安装RabbitMQ【超详细图解】

目录 一、使用brew安装RabbitMQ 二、安装RabbitMQWeb管理界面 三、启动RabbitMQ 一、使用brew安装RabbitMQ 刚好项目要用到RabbitMQ&#xff0c;安装顺便写下安装步骤记录一下以备用 使用brew命令安装&#xff0c;一般Mac会自带这个命令&#xff0c;如没有&#xff0c;…

SQL server-excel数据追加到表

参考文章&#xff1a;SQL server 2019 从Excel导入数据_mssql2019 导入excel数据-CSDN博客 将excel数据导入到SQL server数据库的详细过程 注意&#xff1a;第一行数据默认为数据库表中的字段&#xff0c;所以这个必须要有&#xff0c;否则无法映射导入 问题1&#xff1a;ADD…

YOLOv8独家原创改进:自研独家创新FT_Conv,卷积高效结合分数阶变换

💡💡💡本文自研创新改进:卷积如何有效地和频域结合,引入分数阶傅里叶变换(FrFT)和分数阶Gabor变换(FrGT),最终创新到YOLOv8。 使用方法:1)直接替换原来的C2f;2)放在backbone SPPF后使用;等 推荐指数:五星 在道路缺陷检测任务中,原始map为0.8,FT_Conv为0.82 收…

视频智能分析国标GB28181云平台EasyCVR加密机授权异常是什么原因?

国标GB28181视频汇聚/视频云存储/集中存储/视频监控管理平台EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;实现视频资源的鉴权管理、按需调阅、全网分发、云存储、智能分析等。 近期有用户选择使用加密机进行EasyCVR授…

KaiwuDB 亮相中国国际供应链促进博览会

11月28日&#xff0c;全球首个以供应链为主题的国家级展会——2023 中国国际供应链促进博览会&#xff08;简称“链博会”&#xff09;在北京盛大召开。KaiwuDB 受邀亮相大会&#xff0c;向与会者展示现代数据库技术在数字科技链条中的根基作用&#xff0c;其中分布式多模数据库…

基于springboot实现冬奥会科普平台系统【项目源码+论文说明】

基于SpringBoot实现冬奥会科普平台系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理平台应运而生&…

傅里叶变换及其在机器学习中的应用

​​​​​​​一、介绍 傅立叶变换是一种数学技术&#xff0c;在各个科学和工程领域发挥着关键作用&#xff0c;其应用范围从信号处理到量子力学。近年来&#xff0c;它在机器学习领域发现了新的意义。本文探讨了傅里叶变换的基础知识及其在机器学习应用中日益增长的重要性。 …

Nuxt.js:下一代Web开发框架的革命性力量

文章目录 一、Nuxt.js简介二、Nuxt.js的特点1. 集成Vue.js和Node.js2. 自动代码分割和优化3. 服务端渲染&#xff08;SSR&#xff09;4. 强大的路由管理5. 丰富的插件系统 三、Nuxt.js的优势1. 提高开发效率2. 降低维护成本3. 提高用户体验 四、Nuxt.js在实际应用中的案例1. 电…

HNCTF2022Week1 Reverse WP

文章目录 [HNCTF 2022 Week1]超级签到[HNCTF 2022 Week1]贝斯是什么乐器啊&#xff1f;[HNCTF 2022 Week1]X0r[HNCTF 2022 Week1]你知道什么是Py嘛&#xff1f;[HNCTF 2022 Week1]CrackMe[HNCTF 2022 Week1]给阿姨倒一杯Jvav[HNCTF 2022 Week1]Little EndianNSSCTF{Littl3_Endi…

基础算法学习

文章目录 快速排序归并排序二分浮点数二分 高精度BigIntegerBigDecimal 前缀和差分双指针位运算离散化区间合并 快速排序 确定分界点x &#xff08;可以是左边界&#xff0c;右边界&#xff0c;中间随机&#xff09;将小于等于x的数放到左边&#xff0c;大于等于x的放右边递归…

广州华锐视点:3D毒品预防专题教育平台帮助青少年提升拒毒意识

随着科技的不断发展&#xff0c;人们的生活方式也在不断地改变。在这个信息爆炸的时代&#xff0c;传统的普法教育方式已经无法满足人们的需求。为了适应这一变化&#xff0c;越来越多的教育机构开始尝试利用现代科技手段进行普法教育。其中&#xff0c;3D毒品预防专题教育平台…

跨界融合,科技耕耘:MTX基金公司与ICG共塑全球农业科技新景观

在全球经济快速发展的当下&#xff0c;农业科技创新成为了社会进步的重要推动力。MTX基金公司对ICG的投资是在这一背景下的战略决策&#xff0c;不仅寻求经济效益&#xff0c;更承载着改善全球农业生产、实现食品安全与环境保护的使命。 1、战略投资&#xff1a;文化情怀与全球…

开发知识点-Maven包管理工具

Maven包管理工具 SpringBootSpringSecuritydubbo图书电商后台实战-环境设置&#xff08;JDK8, STS, Maven, Spring IO, Springboot&#xff09;点餐小程序Java版本的选择和maven仓库的配置视频管理系统&&使用maven-tomcat7插件运行web工程SpringTool suite——maven项目…

vue2常见的语法糖

Vue.js 2 提供了一些语法糖&#xff08;syntactic sugar&#xff09;来简化常见的操作。以下是一些 Vue.js 2 中常用的语法糖&#xff1a; v-bind 简写&#xff1a; <!-- 完整语法 --> <a v-bind:href"url">Link</a><!-- 简写 --> <a :hr…

【MySQL数据库】SQL查询语句总结

目录 一、查询数据 1.1 基本查询语句 1.2 表单查询 1.3 WHERE子句 1.3.1 IN关键字查询 1.3.2 Between查询范围 1.3.3 Like匹配查询 1.3.4 AND多条件查询&#xff08;等同于&&&#xff09; 1.3.5 OR多条件查询&#xff08;等同于||&#xff09; 1.3.6 LIMIT子句 1.3.7 对…

tsx语法

安装JSX库 安装完之后在vite.config.ts进行插件使用&#xff0c;代码如下&#xff1a; 然后就可以愉快的使用TSX来开发Vue组件了&#xff0c;下面主要说一下SFC和TSX的部分区别。 基本语法对照 defineComponent 和 setup setup中函数的返回值有多种方式&#xff0c;可以直接…

vuepress-----9、PWA

# 9、PWA 使用babel 的插件形式 [vuepress/pwa,{serviceWorker: true,updatePopup: {message: "New content is available.",buttonText: "Refresh"}}]提供 Manifest 和 icons (opens new window) 拷贝到public目录下 发布后出现 service workers [外链图片…

Apache Flink(二):数据架构演变

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

promis.all的异步使用

基础 参考 https://blog.csdn.net/qq_52855464/article/details/125376557 简单来说 Promise.all是处理接口返回方法异步的&#xff0c;能够使得接口的获取顺序得到控制&#xff0c;不会出现数据为空的情况 使用 先执行jianshigetGroups->groupIds-> const promises2 …

docker搭建rabbit集群

1.去rabbitMQ官网拉去images 我当前使用的是最新版本的镜像&#xff1a;rabbitmq:3.12-management 2.创建一个集群专用网络 docker的容器相互隔离是不可通信的&#xff0c;我们自行创建一个网络后&#xff0c;创建容器时 给他们放在一起&#xff0c;就可以通信了。 docker netw…