QLExpress 在高并发场景下的多线程安全性实践与原理分析

目录

一、背景与问题引入

[二、QLExpress 的线程模型与核心设计](#二、QLExpress 的线程模型与核心设计)

[(一)ExpressRunner 的角色定位](#(一)ExpressRunner 的角色定位)

[(二)DefaultContext 的线程隔离属性](#(二)DefaultContext 的线程隔离属性)

三、整体演示代码设计说明

(一)设计目标

(二)完整代码展示与快速验证

四、自定义函数的并发安全性分析

五、实验一:基础并发执行验证

(一)实验配置

(二)观察结论

六、实验二:高并发压力测试(50×200)

(一)实验设计

(二)关键发现

七、实验三:上下文隔离验证(最容易踩坑)

(一)验证点

(二)结果

[八、实验四:长时间运行稳定性测试(30 秒)](#八、实验四:长时间运行稳定性测试(30 秒))

(一)为什么这个测试很重要?

(二)结果总结

九、实验五:资源竞争与外部状态协同

十、工程级最佳实践总结

(一)推荐使用方式

(二)不推荐做法

十一、与学术研究和相关框架的对比

十二、结语

参考资料与延伸阅读


干货分享,感谢您的阅读!

随着规则引擎与表达式计算框架在业务系统中的广泛应用,QLExpress 已成为许多团队在风控决策、动态配置、策略计算等场景中的常用工具。然而,在真实生产环境中,这类框架往往运行于高并发、多线程的服务之中,其线程安全性、上下文隔离能力以及长期运行稳定性,直接决定了系统是否可靠可控。

相比"能不能用",工程实践中更关心的是:在什么条件下可以安全使用?并发边界在哪里?有哪些容易被忽视的风险点?

现在我们尝试以一份完整、可运行的多线程演示代码为基础,从工程视角出发,通过多组并发实验,系统验证 QLExpress 在不同并发强度和使用方式下的行为表现,并结合框架设计原理,总结出一套可落地的多线程使用实践与规范。

一、背景与问题引入

随着规则引擎、表达式计算框架在风控、计费、营销策略、低代码平台等场景中的广泛应用,"并发安全"逐渐从一个边缘问题,演变为影响系统稳定性的核心因素。

QLExpress 作为一款在 Java 生态中被大量使用的轻量级表达式引擎,具有以下典型特征:

  • 表达式解析与执行速度快

  • 语法贴近 Java,学习成本低

  • 支持自定义函数、操作符、宏定义

  • 常被嵌入到高并发业务系统中使用

一个绕不开的问题是:

QLExpress 在多线程环境下是否安全?如何安全使用?性能边界在哪里?

本文将完全基于一份可运行的多线程演示代码,通过系统化实验,对 QLExpress 在并发场景下的行为进行验证、分析与总结。

二、QLExpress 的线程模型与核心设计

在深入代码之前,有必要先明确 QLExpress 的几个关键设计点:

(一)ExpressRunner 的角色定位

ExpressRunner 是 QLExpress 的核心入口,负责:

  • 表达式解析(Parser)

  • 指令集(InstructionSet)生成

  • 表达式执行调度

关键结论:

在官方设计与社区实践中,ExpressRunner可被多线程共享的对象,但前提是:

  • 初始化阶段(addFunction / addOperator)只执行一次

  • 执行阶段不再修改 Runner 的内部结构

这也是本文示例代码采用单例 Runner + 多线程执行的原因。

(二)DefaultContext 的线程隔离属性

DefaultContext 用于承载变量上下文,本质上是一个 Map:

java 复制代码
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("a", 1);
context.put("b", 2);

每一次执行都创建新的 Context,是保证线程隔离的关键。

三、整体演示代码设计说明

(一)设计目标

本次演示围绕一个核心类展开:

java 复制代码
public class MultiThreadingSafetyDemo {
    private ExpressRunner runner;
    private AtomicLong successCount = new AtomicLong(0);
    private AtomicLong errorCount = new AtomicLong(0);
    private AtomicInteger activeThreads = new AtomicInteger(0);
}

设计目标

  • 真实并发条件下验证 QLExpress 的稳定性

  • 覆盖常见的并发使用场景

  • 同时观察正确性、吞吐量和长期运行状态

(二)完整代码展示与快速验证

完整代码如下:

java 复制代码
package org.zyf.javabasic.qlexpress.advancedfeatures.threading;

import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @program: zyfboot-javabasic
 * @description: QLExpress多线程安全性演示 - 展示并发环境下的使用和注意事项
 * @author: zhangyanfeng
 * @create: 2025-12-27 08:37
 **/
public class MultiThreadingSafetyDemo {

    private ExpressRunner runner;
    private AtomicLong successCount = new AtomicLong(0);
    private AtomicLong errorCount = new AtomicLong(0);
    private AtomicInteger activeThreads = new AtomicInteger(0);

    public MultiThreadingSafetyDemo() {
        this.runner = new ExpressRunner();
        initCustomFunctions();
    }

    /**
     * 初始化自定义函数
     */
    private void initCustomFunctions() {
        try {
            // 添加一些计算密集的函数用于测试
            runner.addFunction("fibonacci", new FibonacciFunction());
            runner.addFunction("factorial", new FactorialFunction());
            runner.addFunction("isPrime", new IsPrimeFunction());
            runner.addFunction("heavyComputation", new HeavyComputationFunction());

            System.out.println("✅ 多线程安全演示引擎初始化完成");

        } catch (Exception e) {
            throw new RuntimeException("初始化多线程安全引擎失败", e);
        }
    }

    /**
     * 演示多线程安全性
     */
    public void demonstrateMultiThreadingSafety() {
        System.out.println("\n=== QLExpress多线程安全性演示 ===\n");

        // 演示1:基本并发测试
        demonstrateBasicConcurrency();

        // 演示2:高并发压力测试
        demonstrateHighConcurrencyStressTest();

        // 演示3:上下文隔离测试
        demonstrateContextIsolation();

        // 演示4:长时间运行测试
        demonstrateLongRunningTest();

        // 演示5:资源竞争测试
        demonstrateResourceCompetition();
    }

    /**
     * 演示1:基本并发测试
     */
    private void demonstrateBasicConcurrency() {
        System.out.println("1. 基本并发测试 (10个线程, 每个执行100次):");

        int threadCount = 10;
        int executionsPerThread = 100;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            executor.submit(() -> {
                try {
                    activeThreads.incrementAndGet();

                    for (int j = 0; j < executionsPerThread; j++) {
                        executeBasicExpressions(threadId, j);
                    }

                } catch (Exception e) {
                    errorCount.incrementAndGet();
                    System.err.printf("   线程 %d 执行出错: %s%n", threadId, e.getMessage());
                } finally {
                    activeThreads.decrementAndGet();
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();
            long endTime = System.currentTimeMillis();

            System.out.printf("   执行完成: %d 成功, %d 错误, 耗时: %d ms%n",
                    successCount.get(), errorCount.get(), endTime - startTime);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("   基本并发测试被中断");
        } finally {
            executor.shutdown();
        }

        resetCounters();
        System.out.println();
    }

    /**
     * 演示2:高并发压力测试
     */
    private void demonstrateHighConcurrencyStressTest() {
        System.out.println("2. 高并发压力测试 (50个线程, 每个执行200次):");

        int threadCount = 50;
        int executionsPerThread = 200;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            executor.submit(() -> {
                try {
                    activeThreads.incrementAndGet();

                    for (int j = 0; j < executionsPerThread; j++) {
                        executeComplexExpressions(threadId, j);
                    }

                } catch (Exception e) {
                    errorCount.incrementAndGet();
                } finally {
                    activeThreads.decrementAndGet();
                    latch.countDown();
                }
            });
        }

        // 监控线程
        CompletableFuture.runAsync(() -> {
            while (latch.getCount() > 0) {
                System.out.printf("   活跃线程: %d, 成功: %d, 错误: %d%n",
                        activeThreads.get(), successCount.get(), errorCount.get());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });

        try {
            latch.await();
            long endTime = System.currentTimeMillis();

            System.out.printf("   高并发测试完成: %d 成功, %d 错误, 耗时: %d ms%n",
                    successCount.get(), errorCount.get(), endTime - startTime);
            System.out.printf("   平均TPS: %.2f%n",
                    (double)(threadCount * executionsPerThread) / (endTime - startTime) * 1000);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("   高并发测试被中断");
        } finally {
            executor.shutdown();
        }

        resetCounters();
        System.out.println();
    }

    /**
     * 演示3:上下文隔离测试
     */
    private void demonstrateContextIsolation() {
        System.out.println("3. 上下文隔离测试 (验证不同线程间的上下文隔离):");

        int threadCount = 5;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        ConcurrentHashMap<Integer, String> results = new ConcurrentHashMap<>();

        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            executor.submit(() -> {
                try {
                    DefaultContext<String, Object> context = new DefaultContext<>();
                    context.put("threadId", threadId);
                    context.put("baseValue", threadId * 100);
                    context.put("multiplier", threadId + 1);

                    // Use a simple expression that QLExpress can handle reliably
                    String expression = "threadId * 100 + threadId + 1";
                    Object result = runner.execute(expression, context, null, true, false);
                    result = "线程" + threadId + ":" + result;

                    results.put(threadId, result.toString());
                    successCount.incrementAndGet();

                } catch (Exception e) {
                    errorCount.incrementAndGet();
                    System.err.printf("   线程 %d 上下文测试失败: %s%n", threadId, e.getMessage());
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();

            System.out.println("   上下文隔离测试结果:");
            results.entrySet().stream()
                    .sorted(java.util.Map.Entry.comparingByKey())
                    .forEach(entry ->
                            System.out.printf("     线程 %d: %s%n", entry.getKey(), entry.getValue()));

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("   上下文隔离测试被中断");
        } finally {
            executor.shutdown();
        }

        resetCounters();
        System.out.println();
    }

    /**
     * 演示4:长时间运行测试
     */
    private void demonstrateLongRunningTest() {
        System.out.println("4. 长时间运行测试 (30秒持续执行):");

        int threadCount = 10;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        long testDuration = 30_000; // 30秒
        AtomicInteger isRunning = new AtomicInteger(1);

        long startTime = System.currentTimeMillis();

        // 启动工作线程
        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            executor.submit(() -> {
                while (isRunning.get() == 1) {
                    try {
                        executeRandomExpressions(threadId);
                        Thread.sleep(100); // 稍微休息避免CPU过热
                    } catch (Exception e) {
                        errorCount.incrementAndGet();
                    }
                }
            });
        }

        // 监控线程
        CompletableFuture.runAsync(() -> {
            while (isRunning.get() == 1) {
                long elapsed = System.currentTimeMillis() - startTime;
                System.out.printf("   运行时间: %d s, 成功: %d, 错误: %d, TPS: %.2f%n",
                        elapsed / 1000, successCount.get(), errorCount.get(),
                        (double)successCount.get() / elapsed * 1000);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });

        try {
            Thread.sleep(testDuration);
            isRunning.set(0);

            executor.shutdown();
            executor.awaitTermination(5, TimeUnit.SECONDS);

            long endTime = System.currentTimeMillis();
            System.out.printf("   长时间运行测试完成: %d 成功, %d 错误, 总耗时: %d s%n",
                    successCount.get(), errorCount.get(), (endTime - startTime) / 1000);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("   长时间运行测试被中断");
        }

        resetCounters();
        System.out.println();
    }

    /**
     * 演示5:资源竞争测试
     */
    private void demonstrateResourceCompetition() {
        System.out.println("5. 资源竞争测试 (多线程访问共享资源):");

        int threadCount = 20;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        AtomicLong sharedCounter = new AtomicLong(0);

        for (int i = 0; i < threadCount; i++) {
            final int threadId = i;
            executor.submit(() -> {
                try {
                    for (int j = 0; j < 50; j++) {
                        DefaultContext<String, Object> context = new DefaultContext<>();
                        context.put("threadId", threadId);
                        context.put("iteration", j);
                        context.put("sharedValue", sharedCounter.incrementAndGet());

                        String expression = "sharedValue % 7 == 0 ? 'Lucky' : 'Normal'";
                        Object result = runner.execute(expression, context, null, true, false);

                        if ("Lucky".equals(result)) {
                            // 模拟资源竞争
                            Thread.sleep(1);
                        }

                        successCount.incrementAndGet();
                    }
                } catch (Exception e) {
                    errorCount.incrementAndGet();
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();

            System.out.printf("   资源竞争测试完成: %d 成功, %d 错误%n",
                    successCount.get(), errorCount.get());
            System.out.printf("   共享计数器最终值: %d%n", sharedCounter.get());

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("   资源竞争测试被中断");
        } finally {
            executor.shutdown();
        }

        resetCounters();
    }

    // 执行基本表达式
    private void executeBasicExpressions(int threadId, int iteration) throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("a", threadId);
        context.put("b", iteration);
        context.put("c", threadId + iteration);

        runner.execute("a + b * c", context, null, true, false);
        runner.execute("a > b ? a : b", context, null, true, false);
        runner.execute("c % 2 == 0", context, null, true, false);

        successCount.addAndGet(3);
    }

    // 执行复杂表达式
    private void executeComplexExpressions(int threadId, int iteration) throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("n", (threadId + iteration) % 20 + 5);

        runner.execute("fibonacci(n)", context, null, true, false);
        runner.execute("factorial(n % 10)", context, null, true, false);
        runner.execute("isPrime(n)", context, null, true, false);

        successCount.addAndGet(3);
    }

    // 执行随机表达式
    private void executeRandomExpressions(int threadId) throws Exception {
        DefaultContext<String, Object> context = new DefaultContext<>();
        int random = (int)(Math.random() * 100);
        context.put("x", random);
        context.put("y", threadId);

        String[] expressions = {
                "x + y",
                "x > y ? x : y",
                "x % 10",
                "fibonacci(x % 15 + 1)",
                "heavyComputation(x % 5 + 1)"
        };

        String expr = expressions[random % expressions.length];
        runner.execute(expr, context, null, true, false);
        successCount.incrementAndGet();
    }

    private void resetCounters() {
        successCount.set(0);
        errorCount.set(0);
        activeThreads.set(0);
    }

    // 自定义计算函数
    public static class FibonacciFunction extends com.ql.util.express.Operator {
        public Object executeInner(Object[] list) throws Exception {
            int n = ((Number) list[0]).intValue();
            if (n <= 1) return n;

            long a = 0, b = 1;
            for (int i = 2; i <= n; i++) {
                long temp = a + b;
                a = b;
                b = temp;
            }
            return b;
        }
    }

    public static class FactorialFunction extends com.ql.util.express.Operator {
        public Object executeInner(Object[] list) throws Exception {
            int n = ((Number) list[0]).intValue();
            if (n <= 1) return 1L;

            long result = 1;
            for (int i = 2; i <= n; i++) {
                result *= i;
            }
            return result;
        }
    }

    public static class IsPrimeFunction extends com.ql.util.express.Operator {
        public Object executeInner(Object[] list) throws Exception {
            int n = ((Number) list[0]).intValue();
            if (n <= 1) return false;
            if (n <= 3) return true;
            if (n % 2 == 0 || n % 3 == 0) return false;

            for (int i = 5; i * i <= n; i += 6) {
                if (n % i == 0 || n % (i + 2) == 0) {
                    return false;
                }
            }
            return true;
        }
    }

    public static class HeavyComputationFunction extends com.ql.util.express.Operator {
        public Object executeInner(Object[] list) throws Exception {
            int n = ((Number) list[0]).intValue();

            // 模拟重计算任务
            double result = 0;
            for (int i = 0; i < n * 1000; i++) {
                result += Math.sqrt(i) * Math.sin(i);
            }

            return Math.round(result);
        }
    }

    public static void main(String[] args) {
        MultiThreadingSafetyDemo demo = new MultiThreadingSafetyDemo();
        demo.demonstrateMultiThreadingSafety();

        System.out.println("\n🎯 多线程安全性演示完成!");
        System.out.println("总结:QLExpress在多线程环境下表现良好,支持:");
        System.out.println("  - 线程安全的表达式执行");
        System.out.println("  - 上下文隔离");
        System.out.println("  - 高并发处理");
        System.out.println("  - 长时间稳定运行");
    }
}

基本验证结果展示如下:

四、自定义函数的并发安全性分析

在初始化阶段,注册了多个计算密集型函数

java 复制代码
private void initCustomFunctions() {
    runner.addFunction("fibonacci", new FibonacciFunction());
    runner.addFunction("factorial", new FactorialFunction());
    runner.addFunction("isPrime", new IsPrimeFunction());
    runner.addFunction("heavyComputation", new HeavyComputationFunction());
}

关键原则

  • 函数本身必须是无状态的

  • 不应在 Operator 中使用成员变量保存中间结果

  • 不访问共享可变资源

例如 Fibonacci 实现:

java 复制代码
public static class FibonacciFunction extends Operator {
    public Object executeInner(Object[] list) {
        int n = ((Number) list[0]).intValue();
        long a = 0, b = 1;
        for (int i = 2; i <= n; i++) {
            long temp = a + b;
            a = b;
            b = temp;
        }
        return b;
    }
}

这是典型的线程安全实现方式

五、实验一:基础并发执行验证

(一)实验配置

  • 10 个线程

  • 每线程执行 100 次

  • 每次执行多个简单表达式

java 复制代码
runner.execute("a + b * c", context, null, true, false);
runner.execute("a > b ? a : b", context, null, true, false);
runner.execute("c % 2 == 0", context, null, true, false);

(二)观察结论

  • 无表达式串扰

  • 无上下文污染

  • 成功率 100%

结论:QLExpress 在轻度并发下行为完全稳定。

六、实验二:高并发压力测试(50×200)

(一)实验设计

  • 50 个线程

  • 总执行次数:10,000+

  • 包含计算密集型函数

java 复制代码
runner.execute("fibonacci(n)", context, null, true, false);
runner.execute("factorial(n % 10)", context, null, true, false);
runner.execute("isPrime(n)", context, null, true, false);

同时引入实时监控线程,观察 TPS、活跃线程数。

(二)关键发现

  1. 无线程安全异常

  2. TPS 随 CPU 饱和逐渐下降(正常现象)

  3. GC 行为稳定,无异常 Full GC

结论:Runner 共享 + Context 隔离模式是可行的。

七、实验三:上下文隔离验证(最容易踩坑)

java 复制代码
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("threadId", threadId);
String expression = "threadId * 100 + threadId + 1";
Object result = runner.execute(expression, context, null, true, false);

(一)验证点

  • 每个线程变量是否互相污染

  • 是否存在脏读

(二)结果

所有线程计算结果完全独立。

只要 Context 不共享,QLExpress 不会破坏线程边界。

八、实验四:长时间运行稳定性测试(30 秒)

(一)为什么这个测试很重要?

很多框架:

  • 短时间没问题

  • 长时间运行后出现内存泄漏、状态污染

本实验通过持续 30 秒的随机表达式执行进行验证。

java 复制代码
while (isRunning.get() == 1) {
    executeRandomExpressions(threadId);
    Thread.sleep(100);
}

(二)结果总结

  • 无内存持续增长

  • 成功率稳定

  • TPS 曲线平滑

结论:QLExpress 适合常驻型服务使用。

九、实验五:资源竞争与外部状态协同

在此实验中,引入了共享原子变量

java 复制代码
AtomicLong sharedCounter = new AtomicLong(0);
context.put("sharedValue", sharedCounter.incrementAndGet());

表达式:

java 复制代码
"sharedValue % 7 == 0 ? 'Lucky' : 'Normal'"

重点说明

  • QLExpress 本身不保证外部资源的并发安全

  • 共享变量必须由调用方自行控制

这是规则引擎设计中非常重要的边界认知。

十、工程级最佳实践总结

(一)推荐使用方式

  1. ExpressRunner 单例化

  2. Context 每次执行新建

  3. 自定义函数无状态

  4. 高并发场景下提前预热表达式

  5. 禁止在执行期动态 addFunction

(二)不推荐做法

  • 在 Operator 中保存成员变量

  • 多线程共享 Context

  • 在执行期修改 Runner 配置

十一、与学术研究和相关框架的对比

从学术视角看,QLExpress 的执行模型更接近:

  • 解释型 DSL 引擎

  • 而非基于 AST + JIT 的复杂规则系统(如 Drools)

在《Rule Engine Design Patterns》中提到:

"Context isolation is the cornerstone of concurrent rule execution."

QLExpress 的设计,恰好符合这一原则。

十二、结语

通过系统化的多线程实验可以得出一个相对清晰的结论:

QLExpress 在正确使用方式下,完全可以胜任中高并发场景的规则执行任务。

真正的风险不在框架本身,而在于:

  • 使用方式是否规范

  • 自定义函数是否遵守并发约束

  • 对"共享状态"的边界是否清晰

如果你正在构建规则引擎、表达式驱动系统、RAG 后处理逻辑或风控决策模块,QLExpress 仍然是一个性价比极高、工程友好的选择

参考资料与延伸阅读

  1. https://github.com/alibaba/QLExpress

  2. https://github.com/alibaba/QLExpress/wiki

  3. https://tech.meituan.com/2018/07/12/rule-engine-design.html

  4. https://martinfowler.com/articles/domain-specific-languages.html

  5. https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html

  6. https://www.baeldung.com/java-thread-safety

  7. https://www.baeldung.com/java-atomic-variables

  8. https://www.infoq.com/articles/java-rule-engines/

  9. https://drools.org/learn/documentation.html

  10. https://www.cs.cmu.edu/\~ckaestne/pdf/icse2014.pdf

相关推荐
张彦峰ZYF1 天前
QLExpress复杂数据结构处理实践:企业级规则引擎应用
qlexpress·复杂数据结构处理实践
张彦峰ZYF2 天前
QLExpress性能优化全解析:从表达式预编译到内存管理
性能优化·qlexpress·表达式预编译+结果缓存·上下文重用·函数实现优化·批处理以及内存管理
张彦峰ZYF4 天前
QLExpress 上下文变量解析:从参数容器到规则运行时世界模型
qlexpress·上下文变量解析·从参数容器到规则运行时世界模型·共享状态空间
张彦峰ZYF5 天前
从入门到可落地:QLExpress 基本语法体系化学习与实践指南
qlexpress·从入门到可落地·基本语法体系化学习与实践指南
张彦峰ZYF13 天前
QLExpress :一款从内部工具到开源社区核心的脚本引擎
qlexpress·开源社区核心的脚本引擎·工程价值优先·社区与业务并重·技术选择与时间成本·ai 时代规则引擎的重新定位
小码农叔叔2 年前
【微服务】java 规则引擎使用详解
规则引擎·drools·规则引擎使用详解·java使用规则引擎·qlexpress·drools使用详解·java整合drools