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;
    }

    @Override
    public boolean support(Product p) {
        if (StringUtils.isBlank(supportExpr)) {
            return true;
        } else {
            Boolean b = (Boolean) eval(supportExpr, Maps.of("product", p));
            return b != null && b;
        }
    }

    @Override
    public 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 {  
                @Override
                public boolean support(Product p) {
                    return p.getId() % 2 == 1 && p.getCategoryId() > 1000;
                }  
                @Override
                public 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();
    }

    @Override
    public boolean support(Product p) {
        return instance.support(p);
    }

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《尚品甄选》:后台系统——分类品牌和规格管理(debug一遍)

文章目录 一、分类品牌管理1.1 表结构介绍1.2 列表查询1.3 添加功能1.4 修改功能1.5 删除功能 二、商品规格管理2.1 表结构介绍2.2 列表查询2.3 添加功能2.4 修改功能2.5 删除功能 一、分类品牌管理 分类品牌管理就是将分类的数据和品牌的数据进行关联&#xff0c;分类数据和品…

48、Flink DataStream API 编程指南(1)- DataStream 入门示例

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

Web安全漏洞分析-XSS(中)

随着互联网的迅猛发展&#xff0c;Web应用的普及程度也愈发广泛。然而&#xff0c;随之而来的是各种安全威胁的不断涌现&#xff0c;其中最为常见而危险的之一就是跨站脚本攻击&#xff08;Cross-Site Scripting&#xff0c;简称XSS&#xff09;。XSS攻击一直以来都是Web安全领…

微信小程序本地和真机调试文件上传成功但体验版不成功

可能是微信小程序ip白名单的问题&#xff0c;去微信公众平台&#xff08;小程序&#xff09;上设置小程序的ip白名单 1、在本地中取消不校验 然后在本地去上传文件&#xff0c;就会发现控制台报错了&#xff0c;会提示一个https什么不在ip白名单&#xff0c;复制那个网址 2、…

【中间件】DAL中间件intro

中间件middleware 内容管理 intro数据访问层why use DAL中间件主流DAL中间件方案DAL浅析 本文从理论上介绍一下服务化背景下的DAL中间件的理论并浅析相关中间件 cfeng之前work的时候产品发展到分离服务不分库的阶段&#xff0c;所以根本不需要DAL中间件&#xff0c;也没有分布式…

机器学习——决策树

1.决策树 2.熵&#xff08;不确定程度&#xff09; 3.信息增益 & 信息增益比 3.1 信息增益 & 信息增益比 的 概念 3.2 案例解释说明 &#xff13;.&#xff12;.&#xff11;数据集说明 &#xff13;.&#xff12;.&#xff12;计算 &#xff14;&#xff0e;&#x…

软件设计师——计算机组成原理(一)

&#x1f4d1;前言 本文主要是【计算机组成原理】——软件设计师计算机组成原理的习题&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &…

[React] 2023年最新面试题

[React] 2023年最新面试题 1. class 组件与函数组件的区别2. react 18 新特性有那些?新增 createRoot API自动批处理过渡更新新的Hook 3. redux 和 react-redux 的区别4. redux 中间件的原理5. setState 发生了什么 &#xff0c;render 函数做了什么6. 虚拟DOM&#xff0c; Fi…

海外储能认证标准

北美认证 UL9540 代表一个封装完整的储能系统功能安全认证&#xff0c;关注机械测试&#xff0c;电器测试和环境测试 UL9540A 关注消防本身&#xff0c;UL9540A测试主要从电池储能系统安装参数&#xff0c;安装通风要求&#xff0c;消防设施&#xff0c;消防策略和应对措施…

SpringCloud 一

认识微服务 随着互联网行业的发展&#xff0c;对服务的要求也越来越高&#xff0c;服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢&#xff1f; 单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#xff0c;打成一个…

springmvc实验(三)——请求映射

【知识要点】 方法映射概念 所谓的方法映射就是将前端发送的请求地址和后端提供的服务方法进行关联。在springMVC框架中主要使用Controller和RequestMapping两个注解符&#xff0c;实现请求和方法精准匹配。注解符Controller Spring中包含了一个Controller接口&#xff0c;但是…

【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目

解决idea至少创建jdk17项目 问题idea现在只能创建最少jdk17&#xff0c;不能创建java8了吗?解决 问题 idea现在只能创建最少jdk17&#xff0c;不能创建java8了吗? 我本来以为是 IDEA 版本更新导致的 Bug&#xff0c;开始还没在意。 直到我今天自己初始化项目时才发现&…

在日常工作中怎么处理vue项目中的错误的?

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容:vue项目中的错误如何处理 目录 一、错误类型 二、如何处理 后端接口错误 代码逻辑问题 全局设…

亚马逊云与生成式 AI 的融合:未来展望与综述

文章目录 前言生成式AI的定义生成式 AI应用领域AI办公软件AI创意工具AI企业服务AI网络安全AIIT 运维AI软件开发AI数据智能AI数字代理AI金融AI医疗AI教育AI工业AI汽车AI机器人 后记 前言 在当今数据时代&#xff0c;人工智能和云计算已经成为了企业发展和创新的必不可少的工具。…

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

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

顺丰JAVA开发一面—面试实战经验分析【已通过】

文章目录 面试总结面试开始项目相关基础知识反问环节 顺丰JAVA开发一面面试过程中的问题确实涵盖了很多方面&#xff0c;从项目架构到基础知识再到具体技术细节都有所涉及。 面试官的提问风格也是比较开放的&#xff0c;注重考察面试者的深度理解和解决问题的能力。以下是对每个…

vulnhub-dc1靶场

DC1 配置环境vmware17 nat网络配置 下载地址:DC and Five86 Series Challenges - DC-1 攻击机kali与其在同一网段下 ip:192.168.31.131 信息收集 arp-scan -l #内网探测&#xff0c;扫描目标ip发现目标ip192.168.31.135 使用nmap对目标进行扫描 nmap -T4 -sV -O -A -P …

JS获取字符串里最长的回文字符串

方法一 使用双指针配合枚举 /*** param {string} s* return {string}*/ const longestPalindrome s > {const LEN s.lengthif (LEN < 2) {return s}let maxStr /*** param left * param right * returns */const findPalindrome (left, right) > {while (left &…

福德植保无人机:农业科技的未来已来

一、引言 随着科技的不断进步&#xff0c;无人机技术已经深入到各个领域。而在农业领域&#xff0c;福德植保无人机更是引领了科技潮流&#xff0c;为农业生产带来了革命性的改变。今天&#xff0c;让我们一起来了解福德植保无人机的魅力所在。 二、福德植保无人机的优势 高效作…

elasticsearch操作

目录 一、mapping映射属性二、索引库的CRUD2.1 创建索引库和映射2.2 查询索引库2.3 修改索引库2.4 删除索引库2.5 总结 三、文档操作3.1 新增文档3.2 查询文档3.3 删除文档3.4 修改文档3.5 总结 四、RestClient操作索引库4.1 初始化RestClient4.2 创建索引库4.3 删除索引库4.4 …