JHipster RFC-5 解读:在生成器中用包含式语法(Inclusive Syntax)管理属性
【免费下载链接】generator-jhipsterJHipster is a development platform to quickly generate, develop, & deploy modern web applications & microservice architectures.项目地址: https://gitcode.com/gh_mirrors/ge/generator-jhipster
导读
JHipster 的生成器内部同时存在排他式(exclusive)与包含式(inclusive)两种选项风格:前者如skipClient、skipServer,后者如dto A、service A with serviceImpl。混用两种风格会导致生成器内大量重复的业务逻辑与认知负担。RFC-5(JHipster-RFC-5)定义了生成器内部选项数据模型的标准结构:所有选项一律采用包含式语法——未定义时取合理默认值,已定义时遵循用户决策。本文完整翻译并深度解读该 RFC,同时结合本仓库源码(lib/jhipster/application-options.ts、lib/jdl/core/built-in-options/unary-options.ts、generators/base-application/internal/utils.ts等)印证其设计动机与落地路径,帮助读者理解 JHipster 生成器配置模型的演进方向。
文档定位与基本信息
RFC-5 是 JHipster 技术决策文档(RFC 系列)中的一份设计提案,核心元信息如下:
- Feature Name:Inclusive syntax to manage properties in generator
- Start Date:V8(即面向 JHipster V8 版本规划)
- 关联 Issue:jhipster/generator-jhipster#14416
该 RFC 的目标是定义生成器内部选项数据模型的标准结构。需要特别说明的是,该标准并不强制要求 JDL 或 CLI 层立即全面改造——文档原文明确指出"not necessary the jdl or cli, which should only be encouraged for new ones",即 JDL 与 CLI 只需对新增选项鼓励采用新语法,存量选项可以渐进迁移。
动机(Motivation):排他语法与包含语法混用的代价
当前 JHipster 的选项风格不统一:
- 部分选项使用排他语法(exclusive syntax),例如
skipClient(跳过客户端); - 部分选项使用包含语法(inclusive syntax),例如
dto A(为实体 A 生成 DTO)。
两种风格混用会带来三个层面的问题:
- 额外的业务逻辑:生成器必须同时处理"跳过某物"和"启用某物"两种语义,分支判断大量重复;
- 认知负担:代码中容易出现双重否定,例如
if (!skipClient) {},阅读时难以一眼理解逻辑; - 框架能力受限:排他语法天然不利于表达"按需启用能力"这类正向诉求,限制了生成器可扩展的方向。
从本仓库源码可以印证上述成本。在 lib/jhipster/application-options.ts 中,排他选项被集中登记:
SKIP_CLIENT: 'skipClient', SKIP_SERVER: 'skipServer', SKIP_USER_MANAGEMENT: 'skipUserManagement',而 lib/jdl/core/built-in-options/unary-options.ts 将skipClient、skipServer与noFluentMethod、readOnly、filter、embedded一起定义为一元选项(unary options)。一元选项的语义是"选项名出现即生效",这种"出现即否定/即启用"的设计正是排他语法模糊性的来源之一。
此外,在 lib/jdl/core/built-in-options/tokens/application-tokens.ts 中可以看到,skipClient与skipServer为了同时兼容"应用配置项"与"实体选项"两种场景,被迫同时被归类为KEYWORD和UNARY_OPTION:
// This is actually needed as the skipClient & skipServer options are both entity & app options... if (['SKIP_CLIENT', 'SKIP_SERVER'].includes(tokenConfig.name)) { tokenConfig.categories.push(KEYWORD, UNARY_OPTION); }这种"一个选项承担多重身份"的权宜之计,正是 RFC-5 试图根治的复杂度。
指南级说明(Guide-level explanation):两条核心规则
RFC-5 提出的方案非常简洁,选项应当始终是包含式的,并遵循两条规则:
- 如果最终用户没有定义该选项:生成器采用一个合理的默认值(reasonable default);
- 如果最终用户指定了该选项:生成器尊重最终用户的决策。
规则示例:以skipClient迁移为generateClient为例
RFC 原文用一个具体例子说明如何将排他的skipClient迁移为符合规范的包含式选项:
- 一个没有声明任何
generateClient属性的 JDLapplication,将正常生成该应用的前端(采用合理默认值); - 一个声明了
generateClient属性的 JDLapplication,则遵循用户选择——若设置了generateClient: false,则不生成前端。
这一语义与当前仓库中"默认值来源于派生计算"的实现思路一脉相承。在 generators/base-application/internal/utils.ts 中可以看到类似逻辑:
skipClient: application.clientFrameworkReact || application.clientFrameworkVue,即:当客户端框架是 React 或 Vue 时,skipClient会被派生为true——这正是"用户未显式定义时取合理默认"思想的现网形态(此处默认值与所选框架绑定)。同文件 L226 的另一处派生为:
skipClient: !application.clientFrameworkAngular,可以看到,为了计算同一个skipClient,生成器内部需要在多处根据框架类型做反向推导,这正是排他语法造成的认知负担与重复逻辑的实证。
参考级说明(Reference-level explanation):配置落盘模型
在技术层面,RFC 要求:选项的存在性应当体现为 JSON 对象层级的属性。也就是说,无论选项是否被用户显式书写,最终进入生成器的配置 JSON 中都应有对应的布尔/值属性,而默认值同样被物化在 JSON 中。
输入:一段 JDL 示例
RFC 原文给出的 JDL 如下:
application { config: { baseName: "a" skipClient: true } } @ReadOnly entity E { } paginate * with pager except E输出一:.yo-rc.json(应用级配置)
经转换后,生成器使用的(简化)JSON 对象如下:
{ "generator-jhipster": { "baseName": "a", "generateClient": true, "_comment_for_above": "see the change, and as it is a reasonable default, we may not have to specify it" } }关键点:
- 用户书写的
skipClient: true被正向表达为generateClient: true(注意:RFC 原文此处为true,即"生成客户端"这一包含式语义,同时注明这是合理默认值,甚至可能无需显式落盘); _comment_for_above是 RFC 用于说明语义的注释字段,示意"这是合理默认值,将来实现时或许不必显式写入"。
需要强调的是,RFC 中的generateClient: true是设计提案中的示意值,用于表达"包含式 + 合理默认值"的理念;当前仓库实际仍在应用级使用skipClient(默认值定义见 lib/jhipster/application-options.ts,类型为BOOLEAN,见 L271),迁移是 RFC 面向 V8 的演进目标。
输出二:.jhipster/E.json(实体级配置)
同一个 JDL 中,@ReadOnly注解与paginate * with pager except E全局选项转换到实体E的配置如下:
{ "name": "E", "config": { "queryMethods": true, "_comment_for_above": "reasonable default we may not have to specify it", "deleteMethods": false, "_comment_for_above": "result of the readonly option", "saveMethods": false, "_comment_for_above": "result of the readonly option", "generateEntityLayer": true, "_comment_for_above": "another reasonable default, ... as it is a reasonable default we may not have to specify it", "paginate": false, "_comment_for_above": "end user choice" } }这个例子展示了实体级配置如何被"完全展开"为显式属性:
| 属性 | 值 | 来源语义 |
|---|---|---|
queryMethods | true | 合理默认值(用户未指定,可不落盘) |
deleteMethods | false | @ReadOnly注解的结果(只读实体不生成删除方法) |
saveMethods | false | @ReadOnly注解的结果(只读实体不生成保存方法) |
generateEntityLayer | true | 另一个合理默认值(可不落盘) |
paginate | false | 最终用户决策(except E排除了实体 E) |
可见,实体级配置中的每一项最终都被规范化成一个明确的布尔属性,其值要么来自合理默认,要么来自用户显式决策(注解、全局选项、实体级选项),从而让生成器消费配置时无需再做"是否被定义"的猜测。
实测:readOnly 在当前仓库中的落点
RFC 中的@ReadOnly场景在当前仓库中已存在对应实现。在 lib/jdl/core/built-in-options/unary-options.ts 中readOnly是一元选项;相关 JDL 测试夹具(如 lib/jdl/core/test-support/files/annotations.jdl)与解析转换测试(lib/jdl/converters/jdl-to-json/jdl-to-json-option-converter.spec.ts)均覆盖了注解到 JSON 配置的转换路径,感兴趣的读者可继续深入。
缺点(Drawbacks):Blueprint 扩展的代价
RFC 承认该方案存在缺点:带有额外选项的 Blueprint(蓝图)将不得不覆盖对象定义。为此,方案要求在 generator-core 层提供可覆盖的 API,允许 Blueprint 开发者在每一层(应用级、实体级等)添加自己的属性。
从仓库结构看,这一诉求与当前 Blueprint 机制的设计方向一致:generators/base/internal/blueprint.ts提供了蓝图解析与优先级处理逻辑,generators/base-application则承载了应用与实体的可覆盖定义(如 generators/base-application/entity.ts),为"每层可扩展属性"提供了基础的继承与覆盖骨架。
方案论证与备选(Rationale and alternatives):为什么排他语法不可行
RFC 指出,skip 语法存在多种解读方式,且与二元选项(binary options)不兼容。
排他语法的歧义
RFC 原文给出的例子:
application { name: "a", clientFramework: angular } entity A { } skipController A // No entity HTML, No entity Component, No entity Front consumer Service, No entity Back ITest, No entity Front ITest, no entity front test: is this behavior really clear for end users? /* Illegal syntax */ skipService A with serviceImpl // We generate with serviceClass or we do not generate at all?问题一目了然:
skipController A实际隐含了"不生成实体 HTML、不生成实体组件、不生成前端消费服务、不生成后端集成测试、不生成前端集成测试、不生成前端测试"等一长串连锁行为,但对最终用户而言,这些连锁效应完全不可见、不明确;skipService A with serviceImpl这种组合直接就是非法语法——语义上无法确定"是生成serviceClass还是什么都不生成"。
排他语法一旦与"带参数的二元选项"(如with serviceImpl)组合,就陷入表达力不足的困境。
包含式语法的对等表达
同一需求用包含式语法表达为:
application { name: "a", clientFramework: angular } entity A { } forms * except A clientServices * except A iTests * except A service * with serviceClass每一条都是明确的正向指令:
forms * except A:所有实体都生成表单,除 A 以外;clientServices * except A:所有实体都生成前端消费服务,除 A 以外;iTests * except A:所有实体都生成集成测试,除 A 以外;service * with serviceClass:所有实体都生成服务,且采用serviceClass类型。
对比可见,包含式语法把原来隐藏在skipController背后的多条隐式连锁行为显式化为可单独控制、可精确排除的选项,语义透明、可组合性强,且天然支持except这种面向集合的修饰。
先例(Prior art)
该 RFC 的先例即其关联 Issue:jhipster/generator-jhipster#14416,该 Issue 承载了"在生成器中使用包含式语法管理属性"这一需求的社区讨论与背景。
未来可能性(Future possibilities)
RFC-5 提出的包含式属性模型为生成器打开了三个明确的演进方向:
- 按需激活各层(layer):controller、forms、tests 等层次可以独立、按需地启用或关闭——这正是"把隐式连锁行为拆成显式开关"的自然延伸;
- 按需激活 C、R、U、D 能力:Create、Read、Update、Delete 四类 CRUD 能力可独立控制,与 RFC 中
deleteMethods、saveMethods、queryMethods的实体级展开模型直接呼应; - 不止生成 REST,还能生成消息代理(message broker)的生产者与消费者:这将显著增强微服务架构的附加值——当"生成什么"由正向、显式的选项表达时,新增一类产出物(如消息消费者)只需新增对应的包含式选项,而无需发明新的排他语法。
总结:从 RFC 到源码的验证闭环
RFC-5 的核心主张可归纳为三点:选项一律正向表达、未定义时取合理默认、定义时尊重用户决策。本仓库源码从多个侧面印证了这一设计动机:
- 排他选项的集中登记与默认值/类型定义见 lib/jhipster/application-options.ts(
skipClient/skipServer/skipUserManagement默认false、类型BOOLEAN); - 一元选项(含
skipClient、readOnly等)的语义定义见 lib/jdl/core/built-in-options/unary-options.ts; skipClient同时作为应用选项与实体选项带来的归类复杂度见 lib/jdl/core/built-in-options/tokens/application-tokens.ts;- "未显式定义时按框架派生默认值"的现网实现见 generators/base-application/internal/utils.ts;
- 生成器消费
skipClient做条件判断的实例见 generators/client/command.ts(如主题选择仅在!config.skipClient时触发); - 相关转换与渲染行为的测试快照见 generators/client/generators/bootstrap/snapshots/generator.spec.ts.snap 与 generators/entity/snapshots/generator.spec.ts.snap。
对于 JHipster 生成器开发者和 Blueprint 维护者而言,RFC-5 提供了清晰的演进路线:新增选项时优先采用包含式命名(如generateXxx、xxxMethods),并将默认值物化到.yo-rc.json与.jhipster/*.json中,从而逐步消除双重否定、减少分支逻辑,为按需分层生成、按需 CRUD 与消息驱动生成等能力铺平道路。
【免费下载链接】generator-jhipsterJHipster is a development platform to quickly generate, develop, & deploy modern web applications & microservice architectures.项目地址: https://gitcode.com/gh_mirrors/ge/generator-jhipster
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考