Apache KIE 10 介绍与核心概念
介绍 Apache KIE 10 的定位、孵化背景与核心概念,覆盖 Drools 规则引擎、KieContainer/KieSession 对象体系、DRL 语言要点与 kmodule 配置。
Apache KIE 10 介绍与核心概念
一、项目定位:Knowledge Is Everything
KIE 是 "Knowledge Is Everything" 的缩写,它是 Drools(规则引擎)与 jBPM(业务流程管理)的母项目:把这两个老牌引擎的公共能力抽取出来,形成统一的 API、构建体系与运行时治理层。对使用者而言,KIE 不是一个单独要"运行"的软件,而是一套 Java 库与工程约定——你把规则文件(DRL)、流程定义(BPMN2)放进 Maven 工程的资源目录,KIE 的构建机制把它们编译成可执行的 KieBase,运行时通过 KieSession 与之交互。规则引擎解决的问题是经典的"业务规则与代码分离":促销折扣、风控拦截、审批路由这类高频变动的判断逻辑,从 if-else 里搬到声明式的规则文件中,由引擎负责匹配与调度。
把"声明式"三个字再拆开讲,它意味着两重转变。第一重是位置转变:判断逻辑不再散落在服务的各处分支里,而是集中在以 .drl 为后缀的规则文件中,一次业务调整通常只需要改一行规则条件而不用重新发布整个应用(在规则外置的部署形态下)。第二重是方向转变:命令式代码描述"怎么做"(先查 A、再算 B、最后判断 C),规则只声明"什么条件下做什么",而"什么时候轮到哪条规则执行"交给引擎的网络匹配算法决定。第二重转变对团队的心智冲击最大——顺序不再由你写死,而是由事实与规则共同涌现,这正是本文第四节实测里"审计规则在折扣动作之后才被激活"的来源。理解并接受这个方向,是用好规则引擎的前提;不理解而强行用,会把规则文件写成一堆靠 salience 数字硬排顺序的伪命令式脚本,白白付出引擎的学习成本。
KIE 目前处于 Apache 软件基金会的孵化阶段。2023 年底,Red Hat 主导的 Drools 与 jBPM 社区整体进入 Apache 孵化器,组成 KIE podling;10.x 是 Apache 化之后的第一个大版本线,Maven 坐标仍沿用历史悠久的 org.kie 与 org.drools 组。本文以 10.2.0 为基准(Maven Central 实测可解析、全流程可运行),在 Ubuntu 22.04 与 OpenJDK 17 上完成了从依赖引入到规则触发的完整实测:一条定价规则链的四个规则、两个规则触发路径(KieServices 类路径模式与 KieHelper 编程模式)、以及引擎元数据诊断全部真实跑通,文中输出均为逐字实录。
二、发展历程:从 Drools 到 Apache KIE
2.1 前史:Drools 与 jBPM 的二十年
Drools 起源于 2001 年前后的开源规则引擎项目,2005 年前后进入 JBoss 联盟,随 JBoss 被 Red Hat 收购而成为红帽的规则引擎支柱;jBPM 则是同源的活动/流程引擎,覆盖 BPMN2 流程建模与执行。两者在 6.x 版本(2013 年前后)被统一进 KIE 体系:公共的构建与运行时抽象收拢到 org.kie 组,drools 与 jbpm 成为 KIE 家族下的具体实现。这个时期的 KIE 7.x 长期演进,成为金融、电信等行业系统里规则管理的事实标准之一。
6.x 那次统一值得多说两句,因为 10.x 的许多"约定"正是从那时定型的:KieServices/KieContainer/KieBase/KieSession 四层对象模型、kmodule.xml 装配声明、KJAR 的 Maven 化打包(把规则工程变成普通 Maven 构件,由仓库统一分发与版本化),全部在 6.x 落地并沿用至今。今天学习 KIE 10 的核心概念,本质上就是学习 6.x 确立的这套模型——它足够稳定,以至于跨越了三次大版本线而没有伤筋动骨。这也是为什么本文敢于断言"读测试分层就能理解数据流"式的学习路径在 KIE 上同样成立:模型的稳定让知识有很长的保值期。
2.2 Apache 化:孵化器中的 KIE
2023 年底,Drools 与 jBPM 决议进入 Apache 孵化器,以 KIE 为名组建 podling。这次迁移对使用者的直接影响有三个:其一,官网与文档体系迁到 Apache 域名下,历史站点逐步归档;其二,发行物以 Apache 基金会的流程发布,版本号从 8.x(jakarta 化的第一代)推进到 10.x,对齐 Apache 的发行节奏;其三,Maven 坐标保持 org.kie、org.drools、org.jbpm 不变——这是对存量工程最友好的决定,升级主要是版本号与少量 API 调整,而不是坐标搬迁。10.0 于 2024 年下半年进入发布序列,本文实测的 10.2.0 是 10.x 线上的稳定小版本。
2.3 版本线的取舍
对选型者而言需要分清三条线:7.x 是 jakarta 化之前的最后主线,javax 命名空间、JDK 8 可用,存量工程的主流;8.x/9.x 完成了 jakarta.* 命名空间迁移,是过渡代;10.x 是 Apache 化后的正代,基线升到 JDK 17(本文实测 17.0.20.1 全流程通过)。新项目建议直接从 10.x 起步,它同时拥有最干净的命名空间与最长的 Apache 维护预期;7.x 存量工程则不必急于迁移,坐标不变意味着两条线可以在不同工程里长期并存。
给三条线各配一个判断题,帮读者对号入座:工程里 import 的是 javax.validation 还是 jakarta.validation?前者说明整个技术栈还停在老命名空间,KIE 7.x 是阻力最小的选择;后者可以直接考虑 10.x。构建跑在 JDK 8 还是 17?JDK 8 就地用 7.x,不要为升级 KIE 单独升 JDK;JDK 17 则毫无悬念选 10.x。规则资产有没有独立变更的需求?有则任何一条线上都该规划 KJAR 治理,没有就选最省事的类路径模式。三个问题答完,版本与形态基本就定了——技术选型最怕的不是选项少,而是把本可以按部就班的事搅成开放性难题。
三、项目族谱与实测模块构成
KIE 家族按职能分三层:最上层是 org.kie 的公共 API(kie-api,定义 KieServices、KieContainer、KieSession 等核心接口);中间是引擎实现(org.drools 的规则引擎、org.jbpm 的流程引擎);底层是支撑组件(内存编译器、MVEL 表达式引擎、XML 支持等)。一个最小可用的规则工程需要五个坐标,本文实测工程的依赖声明是:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<drools.version>10.2.0</drools.version>
</properties>
<dependencies>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-xml-support</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-mvel</artifactId>
<version>${drools.version}</version>
</dependency>
</dependencies>
mvn dependency:tree 的实测输出揭示了这五个坐标背后的真实模块面(两级节选):
[INFO] com.example:kie-demo:jar:1.0.0
[INFO] +- org.kie:kie-api:jar:10.2.0:compile
[INFO] | \- org.slf4j:slf4j-api:jar:2.0.17:compile
[INFO] +- org.drools:drools-compiler:jar:10.2.0:compile
[INFO] | +- org.drools:drools-drl-parser:jar:10.2.0:compile
[INFO] | | +- org.drools:drools-io:jar:10.2.0:compile
[INFO] | | \- org.antlr:antlr4-runtime:jar:4.13.2:compile
[INFO] | +- org.drools:drools-base:jar:10.2.0:compile
[INFO] | +- org.drools:drools-drl-ast:jar:10.2.0:compile
[INFO] | +- org.drools:drools-kiesession:jar:10.2.0:compile
[INFO] | +- org.kie:kie-memory-compiler:jar:10.2.0:compile
[INFO] | +- org.drools:drools-ecj:jar:10.2.0:compile
[INFO] | | \- org.eclipse.jdt:ecj:jar:3.44.0:compile
[INFO] | +- org.kie:kie-internal:jar:10.2.0:compile
[INFO] | \- org.antlr:antlr-runtime:jar:3.5.2:compile
[INFO] +- org.drools:drools-core:jar:10.2.0:compile
[INFO] | +- org.drools:drools-util:jar:10.2.0:compile
[INFO] | \- org.drools:drools-wiring-static:jar:10.2.0:compile
[INFO] +- org.drools:drools-xml-support:jar:10.2.0:compile
[INFO] | +- com.thoughtworks.xstream:xstream:jar:1.4.21:compile
[INFO] | \- org.drools:drools-commands:jar:10.2.0:compile
[INFO] \- org.drools:drools-mvel:jar:10.2.0:compile
[INFO] +- org.mvel:mvel2:jar:2.5.2.Final:compile
[INFO] \- org.drools:drools-wiring-dynamic:jar:10.2.0:compile
这棵树本身就是理解 10.x 架构的教材:drl-parser 用 ANTLR4 把 DRL 文本解析成 AST(drools-drl-ast);drools-ecj 借 Eclipse 编译器把表达式编译成 JVM 字节码;kie-memory-compiler 支持在内存中完成规则编译而不落盘;wiring-static 与 wiring-dynamic 是两套引擎装配路径;mvel2 负责表达式求值。五个直接坐标中,kie-api、drools-compiler、drools-core 是传统三件套,drools-xml-support 与 drools-mvel 则是 10.x 里容易被忽视的两个必备项——本文实战篇有它们缺失时的真实报错与修复实录。
四、核心对象体系:从 KieServices 到 KieSession
KIE 的运行时 API 是一条清晰的四层链:KieServices(全局工厂,入口单例)→ KieContainer(一个 classpath 或一个 KieModule 的载体,负责持有编译好的 KieBase)→ KieBase(编译后的知识库,不可变、线程安全,创建成本高)→ KieSession(与知识的会话,可变、轻量、按需创建销毁)。实测代码的最小骨架:
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
KieSession session = kc.newKieSession("pricingKS");
session.insert(new Person("张伟", 35, true, 2600.0));
int fired = session.fireAllRules();
session.dispose();
四个关键语义点:getKieClasspathContainer() 把当前工程 classpath 上的全部 kmodule.xml 与规则资源编译成一个 KieContainer,适合规则随应用一起打包的场景;newKieSession 按 kmodule.xml 里定义的名字取会话;insert 把事实对象(POJO)送进工作内存;fireAllRules 让引擎开始匹配并返回触发的规则条数;dispose 释放会话资源。实测中这条链的完整输出(规则文件见第六节):
KieContainer 构建完成,开始创建 KieSession...
[未成年人提示] 李娜 年龄 16,需家长陪同
[成人基础折扣] 张伟 打9折,价格调整为 2340.0
[成人基础折扣] 王芳 打9折,价格调整为 1350.0
[VIP叠加折扣] 张伟 再打95折,价格调整为 2223.0
[高价订单审计] 张伟 当前价格 2223.0 超过 2000,进入人工审核
共触发规则数: 5
最终价格 -> 张伟: 2223.0 李娜: 500.0 王芳: 1350.0
五次触发里藏着两个重要行为:其一,未成年人提示的 salience 设了 10 所以最先触发,其余规则按引擎调度序执行;其二,高价订单审计的打印出现在两条折扣规则把张伟的价格从 2600 推到 2223 之后——规则动作对事实的修改会反馈回匹配网络,这是前向推理链(Rete/Phreak 网络的活性维护)的直接体现,也是规则编程与普通命令式代码最大的思维差异。
逐行核对这个输出还能读出更多引擎细节。触发的对象范围:李娜只命中未成年人提示(她价格 500 不满足折扣条件),王芳只命中成人基础折扣(非 VIP),张伟一个人贡献了剩余三次触发——规则引擎对每个事实独立评估,不存在"一次评估全局生效"的隐式共享。价格的演进路径:2600 × 0.9 = 2340(成人基础折扣),2340 × 0.95 = 2223(VIP 叠加折扣),两条折扣的叠加顺序虽然由引擎调度决定,但乘法交换性让最终结果与顺序无关——把规则写得顺序无关,和把 reducer 写得不依赖值序是同一种工程素养。最值得玩味的是审计规则:张伟的价格在 2600、2340、2223 三个时点都满足 price > 2000,但这条规则只触发了一次(在 2223 时点,触发达 5 次、打印恰 5 行)。原因是规则动作通过 setter 修改事实后引擎重新评估约束,该事实此前挂起的同名激活被取消并按新状态重建——"事实一变、旧激活作废、新激活排队",这是 Drools 防止同规则对同一事实重复触发的默认机制。若业务上确实希望每个价格变动都记录审计,正确做法是把"审计日志"建成独立事实 insert 进工作内存,而不是依赖原规则反复触发。
五、DRL:声明式规则语言
DRL(Drools Rule Language)是 KIE 最核心的表达载体,一条规则由"条件部分 when + 动作部分 then"构成,语义是"当工作内存中的事实满足条件时,执行动作"。本文实测的定价规则(节选两条):
package rules
import com.example.Person
rule "成人基础折扣"
when
$p : Person(age >= 18, price > 1000)
then
$p.setPrice($p.getPrice() * 0.9);
System.out.println("[成人基础折扣] " + $p.getName() + " 打9折,价格调整为 " + $p.getPrice());
end
rule "VIP 叠加折扣"
when
$p : Person(vip == true)
then
$p.setPrice($p.getPrice() * 0.95);
System.out.println("[VIP叠加折扣] " + $p.getName() + " 再打95折,价格调整为 " + $p.getPrice());
end
DRL 的几个要点:package 声明规则所属的逻辑包名,必须与 kmodule.xml 的 packages 过滤规则匹配(本文实测把 package 从 com.example.rules 改成 rules 就是为了与 kbase 的 packages="rules" 对齐——不匹配时规则会被静默丢弃而不是报错,这是新手最常踩的坑);import 引入事实类型;when 里的 Person(age >= 18, price > 1000) 是模式匹配——工作内存中所有 Person 事实逐个检查这两个约束,绑定为 $p;then 里既可以直接调用 Java 方法改状态,也可以调用引擎内置的操作(insert/update/retract)。salience 控制同批激活规则的优先级,数字越大越先触发。
约束表达式的求值方式也值得一说:drools-ecj 模块会把约束编译成 JVM 字节码(依赖树实测可见),因此 Person(age >= 18, price > 1000) 的运行时开销接近手写 Java 判断,而不是解释执行;表达式引擎 mvel2 则服务于动态求值场景。两层机制共同解释了一个工程经验:DRL 约束里尽量用简单字段比较(走编译路径),少在 when 里塞复杂方法调用——后者既拖慢匹配,也让规则的网络优化失效。事实类的约束字段要求有标准 getter(DRL 写 price 引擎读的是 getPrice()),布尔字段例外地映射 isXxx()(实测的 vip == true 读的是 isVip()),这是 POJO 事实类的唯一硬约束。
规则语言的表达力远不止于此:从句(exists/not/accumulate/collect)、时间约束、规则流组、元数据注解等,覆盖了复杂业务判断的全部常见形态。但对入门工程而言,先掌握"模式 + 约束 + 动作 + salience"这一最小集,就能把大部分 if-else 业务搬进规则文件——本文实战篇全部围绕这个最小集展开。
六、kmodule.xml:把资源装配成知识库
KIE 需要知道"哪些资源文件属于哪个知识库、暴露哪些会话",这份装配说明书就是 src/main/resources/META-INF/kmodule.xml。本文实测文件:
<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
<kbase name="pricingKBase" packages="rules">
<ksession name="pricingKS" type="stateful"/>
</kbase>
</kmodule>
三个语义点:kbase 的 name 是逻辑标识,供 newKieSession 间接引用;packages="rules" 声明把 classpath 上所有 package 为 rules 的 DRL 收进这个 kbase——这是按包名聚合而不是按路径聚合,规则文件可以分散在多个 jar 里只要包名一致;ksession 定义会话模板,type="stateful" 表示有状态会话(多次 insert/fire 之间事实持续存活),stateless 则每次调用独立。kmodule.xml 由 drools-xml-support 提供解析能力——10.x 把 XML 解析拆成了独立模块,缺了它 KieContainer 静默构建出空的 KieBase,newKieSession 返回 null,这是本文实测出的第一个集成陷阱。
kmodule.xml 还支持多个 kbase 并存与空 packages 的高级形态:一个工程里可以按业务域定义多个 kbase(各绑不同的 packages),彼此独立编译互不影响;packages 不写则收拢 classpath 上全部规则。对入门工程而言单一 kbase 是最简形态,但理解"多 kbase 隔离"的存在很有价值——它是大型系统里规则资产分域治理的第一块基石,也解释了为什么 kbase 的编译成本值得按域拆分而不是全量共享。
七、两条规则触发路径
10.x 提供两条把 DRL 变成可执行规则的路。其一是类路径模式(KieServices + kmodule.xml),规则作为工程资源随构建打包,适合规则相对稳定的场景,本文第四节的实测即此路线。其二是编程模式(org.kie.internal.utils.KieHelper),规则字符串或资源在代码里动态注入:
KieHelper helper = new KieHelper();
helper.addContent(drl, ResourceType.DRL);
Results results = helper.verify();
if (results.hasMessages(Message.Level.ERROR)) {
System.out.println("规则编译失败: " + results.getMessages());
return;
}
KieSession session = helper.build().newKieSession();
实测这条路径注入一条黑名单规则后插入两个事实、fireAllRules 返回 1,输出:
[黑名单拦截] 老赖 拒绝下单
共触发规则数: 1
KieHelper 路线的价值在动态性:规则来源可以是数据库、配置中心或用户上传,verify 先行校验避免了坏规则污染会话;代价是它属于 org.kie.internal 命名空间(半公开 API),版本间兼容承诺弱于 org.kie.api。生产选型的粗略原则:规则跟着应用发版走用类路径模式,规则要在线变更才考虑 KieHelper 或更完整的 KJAR + Maven 仓库方案。
两条路线还各有一组隐性成本需要放进决策。类路径模式的规则与代码同版本发布,回滚规则等于回滚应用——稳但重,适合规则季度级变动的系统;KieHelper 模式把"规则是否合法"从构建期挪到了运行期,必须自己补上 verify 报告的人机界面与坏规则回滚机制,否则一条打错字的规则就能让会话构建失败。中间态的 KJAR 方案(规则独立成 Maven 构件、运行时从仓库拉指定版本)兼顾了版本化与独立发布,是 Drools Workbench 时代的标准形态,10.x 里依然可用但需要自建仓库治理。规模越大的规则资产,越值得为版本治理投入——三条路线的取舍本质上是"规则资产的发布频率与治理深度"的函数。
八、孵化状态与选型视角
把 KIE 放进技术选型清单时,孵化状态意味着什么需要客观看待。正面:Drools 引擎本身有约二十年的工业打磨,KIE 10.x 的代码主体是从被大量生产系统验证过的 7.x 演进而来,成熟度并非"孵化器新项目"的字面印象;Apache 化后社区治理透明,版本节奏可预期。需要注意:孵化项目没有 1.0 式的"顶级项目稳定性承诺",API 在 10.x 线内仍可能微调(KieHelper 所在的 internal 包尤其如此);商业化支持渠道与传统红帽产品线(Decision Manager 等)需要分别评估。综合建议:以规则引擎为核心诉求的新项目,KIE 10.x 是当前社区版的事实首选;对稳定性要求极高的场景,可用 10.x 最新小版本并锁定版本、固化回归测试(本文实战篇的规则测试思路可直接复用)。
最后补一条风险清单式的提醒,它来自本文实测的第一手观察:10.x 的模块拆分比 7.x 细得多(依赖树里 drools-drl-parser、drools-xml-support、drools-mvel、drools-ecj 各司其职),从 7.x 迁移时不能只换版本号——缺一个支撑模块的报错往往出现在运行期而非编译期(本文两个排障实录都属此类),迁移演练必须以"规则真实触发"为验收标准,而不是编译通过。把这个验收标准写进迁移清单,孵化状态带来的不确定性就被压缩到了可管理的范围。
九、常见问题(FAQ)
1. KIE 和 Drools 是什么关系? KIE 是母项目,提供公共 API 与构建体系;Drools 是它旗下专注规则引擎的实现。日常编码 import 的核心接口(KieServices、KieContainer、KieSession)来自 org.kie:kie-api,规则解析与执行来自 org.drools 各模块。
2. 10.x 需要 JDK 几? 本文实测在 OpenJDK 17.0.20.1 上全流程通过;10.x 已完成 jakarta 化,不再兼容 JDK 8 的 javax 生态,新工程直接以 JDK 17 为基线最省心。
3. 为什么 kmodule.xml 放对了位置还是拿不到 KieSession? 大概率缺 drools-xml-support 依赖。10.x 把 kmodule 的 XML 解析拆到这个模块,缺失时 KieContainer 照常构建但内部 KieBase 为空,newKieSession 返回 null,后续 insert 才抛空指针——本文安装篇有完整报错与修复实录。
4. DRL 语法没错但规则就是不触发? 按序检查三点:DRL 的 package 是否命中 kmodule 的 packages 过滤(不匹配会静默丢弃);事实是否 insert 进了会话;约束字段是否用了 getter 语义(Person(price > 1000) 匹配的是 getPrice())。引擎对不满足条件的事实保持沉默是正常行为,不是异常。
5. KieBase 和 KieSession 该怎么分配? KieBase 创建成本高(要完成规则编译与网络构建)但线程安全、可长期持有;KieSession 轻量但持有工作内存状态。惯例是应用启动时建一次 KieBase,按请求或按业务单元创建 KieSession,用完 dispose。
6. 规则动作里修改了事实,会发生什么? 修改(经 setter 或 update 通知)会让该事实重新参与匹配,可能激活新的规则——本文实测里张伟的价格被两条折扣规则依次修改后,价格超过 2000 的审计规则才被激活,就是这一机制的体现。要避免无限循环需在规则上设计终止条件或用 no-loop 属性。
7. KieHelper 是正式 API 吗? 它在 org.kie.internal 命名空间,属于"公开但弱承诺"的层:可用、文档不少,但版本间兼容性弱于 org.kie.api。动态注入规则用它最方便;只用静态规则的工程可以完全避开它。
8. 孵化器项目能用在生产吗? 能,但要知情使用:KIE 10.x 的引擎主体继承自长期生产验证的 Drools 线,风险主要在 API 微调与周边工具链的跟进速度。锁定小版本、固化回归测试、关注孵化器公告,是可控的三板斧。
9. 与直接写 if-else 相比,规则引擎的性能代价有多大? 小规模规则(几十条)差异可忽略;大规模规则(成百上千条)下引擎的 Rete/Phreak 网络通过节点共享与增量匹配摊薄成本,通常优于手写的串行判断链。真正的差异在可维护性而非性能——规则的增删改不触碰主代码,这才是选规则引擎的主因。
10. jBPM 也在 KIE 里,同一工程能同时用规则和流程吗? 架构上可以(共享 kie-api 与 KieBase 体系),但本文实测范围仅覆盖 Drools 规则线;流程引擎需要额外引入 jbpm 系列坐标,建议按独立依赖单独评估。
11. 10.x 的坐标为什么还是 org.kie? Apache 化迁移刻意保持了坐标连续性,org.kie、org.drools、org.jbpm 三个组原样保留。对使用者这是最重要的兼容承诺:升级是改版本号,不是全局替换坐标。
12. 在哪里找 10.x 的可靠文档? Apache 孵化器官方站点与项目 Wiki 是第一来源;Drools 历史文档中与坐标、DRL 语法相关的部分仍高度适用(语言层面 10.x 与 7.x 同源)。本文的实测行为可作为 10.2.0 的行为参照。
十、参考链接
- Apache KIE(孵化中)官网:https://kie.apache.org/
- Apache 孵化器项目列表(KIE):https://incubator.apache.org/projects/kie.html
- Maven Central 上的 kie-api 10.2.0:https://central.maven.org/artifact/org.kie/kie-api/10.2.0
- Maven Central 上的 drools-compiler 10.2.0:https://central.maven.org/artifact/org.drools/drools-compiler/10.2.0
- Drools 官方文档(历史主线):https://www.drools.org/
- OpenJDK 17 发布页:https://openjdk.org/projects/jdk/17/
- Apache Maven 官方文档:https://maven.apache.org/guides/