模块导出必须精确限定包路径,禁止通配;OpenAPI与模块注解需双向约束;文档URL须含版本号;接口变更需字段级diff告警阻断合并。

模块导出声明必须显式限定包路径
模块的 exports 语句不是装饰性语法,而是文档生成的事实依据。如果写成 exports com.example.service;,javadoc 就会把整个包及其所有子包都纳入公共 API 文档;但若实际只希望暴露 com.example.service.api,就必须精确写出该路径,否则内部工具类、测试辅助类甚至临时调试代码都会被误认为是可调用接口。
常见错误是把模块声明当成“开关”而非“契约”:开发者为图省事用通配或宽泛路径导出,结果文档里出现一堆 internal、impl、testutil 包,前端或跨团队调用方真会尝试引用——因为文档里有,就默认它是可用的。
- 导出前先人工检查包结构,确认只有
api、dto、exception这类明确面向外部的包在列 - 禁止使用
exports com.example.service.*;这类通配写法 - CI 流程中加入静态检查:扫描
module-info.java,对非白名单包路径的exports报告警告
OpenAPI + 模块注解双驱动文档生成
纯靠 javadoc 生成的模块文档只能反映 Java 类签名,无法表达 HTTP 路由、请求体结构、状态码含义等 API 语义。而仅靠 OpenAPI(如 Swagger)又容易脱离模块边界,导致文档里出现本不该跨模块调用的内部端点。
真正可行的做法是让 OpenAPI 定义(openapi.yaml)与模块声明形成约束闭环:
- 每个模块对应一个独立的 OpenAPI 文件,文件名与模块名一致(如
com.example.auth/openapi.yaml) -
openapi.yaml中的info.title必须与module-info.java的模块名完全匹配 - CI 构建时校验:所有
@GetMapping、@PostMapping注解所在的类,其所属包必须被该模块的exports显式声明 - 文档站点聚合时,只合并那些通过上述校验的模块 OpenAPI 文件
模块版本号必须绑定到文档 URL 路径
当多个团队共用同一个模块(比如 com.example.logging),不同项目依赖的版本可能不同。如果文档站点不带版本标识,访问者看到的永远是最新的 main 分支内容,而实际线上跑的是 v1.2.4——这种错位比没有文档更危险。
解决方案不是靠人去记住“查哪个分支”,而是强制文档 URL 包含版本号:
- 静态文档站点目录结构按
/docs/{module-name}/{version}/index.html组织 - CI 发布文档时,从
pom.xml或build.gradle提取version值,动态生成路径 - 模块的 Maven 坐标(如
com.example:logging:1.3.0)应直接出现在每页文档页眉 - 禁止提供 “最新版” 跳转链接——它本质是不可靠的别名,会掩盖真实版本差异
接口变更必须触发文档字段级 diff 告警
最隐蔽的问题不是文档没更新,而是“看起来更新了,但漏改了一处”。比如新增一个查询参数 tenantId,开发者改了代码和 OpenAPI 描述,却忘了同步更新 javadoc 中的 @param 注释,或者漏掉响应 DTO 字段的说明。
这类问题无法靠人工 review 发现,必须自动化捕获:
- 每次 PR 提交时,运行工具对比本次修改前后
openapi.yaml与对应 Java 接口类的 javadoc 注释 - 重点检查:新增/删除的参数是否在两者中同时存在;字段类型是否一致(
StringvsOptional<String>算不一致);@return描述是否覆盖了所有可能返回值 - diff 工具输出必须定位到具体行号,例如:
UserService.java:42: @param userId missing in javadoc but present in openapi.yaml - 该检查失败即阻断合并,不接受“文档后续补”的理由
模块共享的本质不是共享代码,是共享契约。而契约的效力,取决于它被写死在哪儿、谁有权修改、以及修改后如何被所有人感知——这些细节,比文档长得好不好看重要得多。

















